DEV Community

Hassan Elsayed
Hassan Elsayed

Posted on

HTB Broken Authentication — Skill Assessment Writeup

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
Enter fullscreen mode Exit fullscreen mode

Server response:

Unknown username or password
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Breaking it down:

  • -w — wordlist of common usernames
  • -u — target login endpoint
  • -X POST — HTTP method
  • -d — POST body, FUZZ gets 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]
Enter fullscreen mode Exit fullscreen mode

✅ 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.
Enter fullscreen mode Exit fullscreen mode

A different message than before, confirming:

  • gxxxxs is 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
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode

Result:

dWXXXXXXXX13 [Status: 302, Size: 0, Duration: 656ms]
Enter fullscreen mode Exit fullscreen mode

✅ 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:

  1. Brute force the OTP — unknown length (4 vs 6 digits), not efficient
  2. 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{...}";
Enter fullscreen mode Exit fullscreen mode

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:

  1. Enable Burp Proxy
  2. In Proxy → HTTP History, find the request to /profile.php
  3. 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]
Enter fullscreen mode Exit fullscreen mode

Modified response:

HTTP/1.1 200 OK
Location: /profile.php
Content-Length: 3647
[protected page HTML content here]
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

🎯 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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
// Secure
if (!$_SESSION['2fa_verified']) {
    header("Location: /2fa.php");
    exit; // critical
}
include('profile_content.php');
Enter fullscreen mode Exit fullscreen mode

Defense Recommendations

  • Always call exit; right after header() 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

  1. Small vulnerabilities compound — none of these alone is critical, but chained together they gave full auth bypass
  2. Never trust client-side redirects for access control — always validate server-side
  3. Error messages leak information — even subtle differences enable enumeration
  4. Password policy ≠ password security — complexity rules don't stop reused/breached passwords
  5. 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)