Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Authentication — Timing-Based Username Enumeration, Brute Force
Difficulty Practitioner
Objective Enumerate a valid username, brute-force that user's password, then access their account page
Note Own credentials wiener:peter

Username Enumeration via Response Timing

I started by logging in as wiener:peter to confirm the app worked normally.

Screenshot

After logging out and deliberately failing several login attempts with the wrong password, the app hit me with a lockout: "You have made too many incorrect login attempts. Please try again in 30 minute(s)."

Screenshot

The lockout is IP-based, which means adding an X-Forwarded-For header to spoof the source address sidesteps it entirely — each request with a distinct value looks like it's coming from a different client:

POST /login HTTP/2
Host: 0a53009504f0058c80d812fd005c00a4.web-security-academy.net
Cookie: session=WEsiJGaJzdHQuFsre7efri8BNlByJDFo
X-Forwarded-For: 127.0.0.2

username=wiener&password=teto
Screenshot

With the block bypassed, the next thing to establish was the timing signal. Submitting a very long string in the password field caused a noticeably slower response — the app was apparently running a real password hash comparison for valid usernames, while invalid ones short-circuit before reaching that step. That difference in processing time is the leak, and it shows up even when the response body and status code look identical across all attempts.

With both pieces understood — the IP bypass and the timing behavior — I set up a pitchfork attack in Intruder with two payload positions: the X-Forwarded-For value (sequential numbers from 3 to 103, one per request) and the username candidate list, paired one-to-one. Resource pool set to 1 concurrent request to keep everything sequential and avoid races.

Screenshot
Screenshot

Sorting results by response time, affiliate stood out immediately with a 1000+ ms response — well above everything else. Since each username only got a single attempt against a fresh spoofed IP, the lockout never triggered, and the timing difference did all the work of identifying it as valid.

With affiliate confirmed, I ran a second pitchfork attack — a fresh range of X-Forwarded-For values (103 through 203) paired against the candidate password list. The same structure: one unique IP per attempt, 1 concurrent request.

Screenshot

The password 123123 returned a 302, confirming the working combination as affiliate:123123. Because 127.0.0.1 was never used directly during either attack phase, there was no block on the real IP to worry about. Logging in went through cleanly.

Screenshot

Lab solved

Resources