September 7, 2026
Why a 100-Character Password Broke This Login Form: Username Enumeration via Response Timing
Lab three in my Web Security Academy series β where the vulnerability isnβt in what the app says, but in how long it takes to say it
By Kaustubh Asthana
6 min read
My first two labs in this series were about spotting differences in what a login form says back to you β different wording, then a single differing full stop. This lab is a different beast entirely: the login form gives back the exact same response text no matter what you enter. The only thing that differs is how long it takes to respond.
This is "Username enumeration via response timing," and solving it taught me two things I want to unpack before walking through the steps: why the size of your test payload can determine whether an attack even works, and why choosing the right Burp Intruder attack type matters as much as the payload itself.
Why the Response Time Differs at All
Before touching Burp, it's worth understanding why this vulnerability exists. Many login systems only run the expensive part of password verification β hashing the submitted password and comparing it β if the username actually exists. If the username doesn't exist, the app can fail fast and skip that work entirely.
Password hashing algorithms designed for this purpose (like bcrypt) are deliberately slow, and critically, the time they take to run scales with the length of the input being hashed. That's the whole mechanism this lab exploits: a valid username triggers the slow hashing step, an invalid one doesn't, and that gap shows up as a measurable difference in response time.
Why 100 Characters and Not 10
This is the part I want to explain properly, because it's the difference between the attack working and not working at all.
If the timing gap between "hashed a password" and "didn't hash a password" is only a few milliseconds, that gap can easily get swallowed by ordinary network jitter, server load, or connection variance. Ten short-password requests might show response times that overlap so much you genuinely can't tell which one hit a valid username β the signal is real, but it's buried in noise.
A much longer password β in this case, 100 characters β makes the hashing step itself take measurably longer for a valid username, because the hashing algorithm has more input to process. That stretches the timing gap wide enough that it becomes visually obvious even when just eyeballing a results table, rather than something you'd need statistical analysis to detect. I tried this with a short password first and the response times were far too close together to draw any reliable conclusion. Switching to a 100-character password made the valid username's response time stand out clearly from the rest.
The lesson here generalizes beyond this one lab: when you're trying to detect a timing-based signal, exaggerate the condition that produces it. A subtle timing bug can become an obvious one just by giving the vulnerable code more work to do.
The Lockout Wrinkle
Before I could run a clean attack across the full username list, I hit a wall: after three failed login attempts, the application locked out my IP address entirely. That's a completely reasonable defensive control on its own β rate limiting by IP is standard practice β but it meant I couldn't just fire dozens of requests from Burp Intruder in a row without getting blocked partway through.
The workaround was to spoof a different IP address on every single request, using the X-Forwarded-For header. Many applications trust this header to determine the "real" client IP when they sit behind a proxy or load balancer, and if the lockout logic keys off that header instead of the actual TCP connection, you can sidestep the block entirely by sending a fresh, fabricated value on every request β the server never sees the same "IP" twice, so it never accumulates enough failed attempts from any single one to trigger a lockout.
That meant this attack needed a second marked position alongside whatever I was actually testing: the X-Forwarded-Forheader, paired with either the username or the password depending on the phase. The value I wasn't testing that round β password in phase one, username in phase two β didn't need to be marked as a position at all. It just stayed typed into the request as a plain, static value, untouched by Intruder.
A Quick Detour: Burp Intruder's Attack Types
Before I get to how I actually set this attack up, it's worth covering something I had to think through carefully here: Burp Intruder offers four attack types, and picking the wrong one either breaks the attack or wastes an enormous number of requests.
- Sniper β uses a single payload set, cycling through it one position at a time while every other marked position stays at its original value. This is what I used in labs one and two, where only the username needed to vary.
- Battering ram β uses a single payload set, but inserts the same payload into every marked position simultaneously. Useful when you need one value repeated in multiple places in a request, like a username appearing in both a header and the body.
- Pitchfork β uses multiple payload sets, one per marked position, and iterates through them in parallel β position 1's first item pairs with position 2's first item, position 1's second item pairs with position 2's second item, and so on. The lists move in lockstep.
- Cluster bomb β uses multiple payload sets, but tries every possible combination of them. If you have 50 usernames and 50 passwords, that's 2,500 requests, not 50.
For this lab, I initially thought about cluster bomb, since I had two positions marked each round β IP and whichever field I was testing. But that would mean testing every combination of fake IP against every candidate in the other list, which explodes fast: with the IP needing to change on every single request just to avoid getting locked out again, a cluster bomb approach would have demanded a unique IP for every combination in the grid, rather than one per actual test I cared about.
Pitchfork was the right call. I needed the IP to change in lockstep with whichever value I was actually testing β username first, then password β while the third value (whichever one wasn't being tested that round) simply stayed as a fixed string already typed into the request, never marked as a position at all. Pitchfork's parallel iteration handles the two marked positions together: one fresh IP paired with one candidate value per request, no wasted combinations.
Setting Up the Attack
With that reasoning settled, here's what I actually did, in two phases.
Phase 1 β finding the valid username:
- Captured a login request with a placeholder username, and typed a 100-character password directly into the password field as a plain, static value β this field was never marked as a position, it just stayed as-is for every request
- Added an
X-Forwarded-Forheader to the request - Sent the request to Burp Intruder
- Marked two positions: the
X-Forwarded-Forvalue and the username field - Set the attack type to Pitchfork
- Loaded the candidate username list as one payload set (a straightforward preset list of candidate strings)
- Set the other payload set (
X-Forwarded-For) to Burp's built-in Numbers payload generator, but only applied to the last octet of the address β the header stayed as a fixed base like1.2.3.with just that final number auto-incrementing on each request, which was enough to make every request look like it came from a distinct IP
Reading the Results
Once the attack ran, I sorted Burp's results table by the response time column. Scanning down the sorted list, one username's response time was well clear of the tight cluster the rest sat in β that outlier was the valid username: the only request where the server actually ran the expensive hashing step, and the IP rotation meant none of the earlier failed attempts had gotten me locked out before I reached it.
Phase 2 β Confirming the Password
With the username confirmed, cracking the actual password meant running a second pitchfork attack. This time, the username I'd just confirmed went into the request as a static value β no longer marked as a position β while two positions were marked instead:
- The
X-Forwarded-Forheader β the same last-octet Numbers payload, still auto-incrementing to dodge the lockout - The password field β a preset list of candidate passwords
The request that returned a redirect to the account page (rather than the standard login error) confirmed the correct password and completed the lab.
Takeaways from Lab 3
Timing attacks are real, but signal strength depends on your payload. A vulnerability that's invisible with a short test value can become obvious once you give the underlying operation more work to do.
Rate limiting by IP isn't rate limiting if the IP is spoofable. Trusting a client-supplied header like X-Forwarded-For for security decisions is a separate, common vulnerability in its own right β and it's often the thing that makes an otherwise-solid defensive control (like a lockout policy) completely bypassable.
Attack type is a design decision, not a formality. Cluster bomb would have technically "worked" here, just at a massive and unnecessary cost β one that only gets worse as you add more positions. Understanding what each attack type actually does β not just picking the one that lets you mark the most positions β is what separates efficient testing from brute-forcing your way through a lab.