Press enter or click to view image in full size
After diving deep into file upload vulnerabilities part 1 and part 2, I thought I had a good grasp on web application security. But then I stumbled upon a different beast: Race Conditions. These aren’t about tricking file types or extensions; they’re about exploiting the very fabric of how applications process requests over time.
Imagine a website where you can get a one-time discount. You apply the code, and it works. Simple, right? But what if you could apply that discount not just once, but multiple times, turning a 20% off into a 200% off? That’s the magic (or menace) of a race condition, specifically a ‘limit overrun’.
In this post, I’ll walk you through how I exploited a classic limit overrun race condition, drawing insights from PortSwigger’s Web Security Academy labs. This isn’t just theoretical; these flaws can lead to serious financial losses for businesses and unexpected gains for attackers.
At its core, a race condition occurs when a system’s behavior depends on the sequence or timing of uncontrollable events. In web applications, this often means sending multiple requests to the server almost simultaneously, hoping they hit the server in a specific, vulnerable order.
The ‘Limit Overrun’ Scenario
Let’s take that discount code example. A typical application process for applying a one-time discount might look like this:
1. Check: Has the user already used this code?
2. Apply: If not, apply the discount to the order.
3. Update: Mark the code as used in the database.
Press enter or click to view image in full size
This seems robust. If you try to reuse the code, the initial check should stop you. But what if two requests arrive at the server so close together that the ‘check’ happens for both before the ‘update’ from the first request has finished?
Press enter or click to view image in full size
This creates a tiny ‘race window’ a temporary sub-state where the application is vulnerable. If you can send multiple requests within this window, you can bypass the ‘one-time’ limit.
I’ve seen variations of this in real-world scenarios:
- Redeeming a gift card multiple times
- Withdrawing more money than available in an account
- Reusing a single CAPTCHA solution
- Bypassing anti-brute-force rate limits
These are all subtypes of ‘Time-of-Check to Time-of-Use’ (TOCTOU) flaws, where the condition is checked, but then changes before it’s used.
Finding these tiny race windows can be like finding a needle in a haystack. But with the right tools, it becomes much more manageable. Burp Suite’s Repeater, especially with its newer features, is a game-changer for this.
I tackled a lab in PortSwigger’s Web Security Academy where the goal was to purchase a ‘Lightweight L33t Leather Jacket’ for an unintended price by exploiting a discount code race condition. The credentials were wiener:peter.
Press enter or click to view image in full size
First, I logged in and bought the cheapest item possible, making sure to use the provided discount code so that I could study the purchasing flow. In Burp, from the proxy history, I identified all endpoints that enabled me to interact with the cart. For example, a POST /cart request adds items to the cart and a POST /cart/coupon request applies the discount code.
Press enter or click to view image in full size
I also sent the GET /cart request to Burp Repeater and confirmed that without the session cookie, I could only access an empty cart. From this, I could infer that:
The state of the cart is stored server-side in your session.
Any operations on the cart are keyed on your session ID or the associated user ID.
This indicated that there was potential for a collision.
I noticed that if I tried to apply the discount code more than once, I’d get a
Coupon already applied response. This confirmed it was a single-use code.
Press enter or click to view image in full size
To understand the server’s response time, I cleared any applied discount codes from my cart. Then, I sent the POST /cart/coupon request to Repeater. In Repeater, I added the new tab to a group. Then, I right-clicked the grouped tab and selected Duplicate tab, creating 19 duplicate tabs. The new tabs were automatically added to the group.
I sent this group of requests in sequence, using separate connections to reduce the chance of interference. As expected, the first response confirmed that the discount was successfully applied, but the rest of the responses consistently reject the code with the same Coupon already applied message. This established the baseline.
Press enter or click to view image in full size
Now for the real test. I removed the discount code from my cart again. In Repeater, I sent the group of requests again, but this time in parallel, effectively applying the discount code multiple times at once. This is where Burp Repeater’s powerful new capabilities shine, especially with its HTTP/2 single-packet attack, which can send 20–30 requests simultaneously in a single TCP packet, neutralizing network jitter.
Press enter or click to view image in full size
I studied the responses and observed that multiple requests received a response indicating that the code was successfully applied! This was it the race condition was triggered.
Press enter or click to view image in full size
In the browser, I refreshed my cart and confirmed that the 20% reduction had been applied more than once, resulting in a significantly cheaper order. The limit was overrun!
How I found it: By sending multiple requests in parallel using Burp Repeater, I was able to hit the server within the race window, causing the discount to be applied more than once before the database could update.
While Burp Repeater is fantastic for quickly testing parallel requests, some race conditions demand more precision, more attempts, or a more complex sequence. That’s where Turbo Intruder shines. It’s an extension for Burp Suite that allows for highly customized, high-performance attack sequences, often requiring a bit of Python scripting.
It’s particularly powerful because it also supports the single-packet attack technique, just like Repeater, but gives you fine-grained control over how and when requests are sent. If you’re dealing with extremely tight race windows or need to orchestrate a complex attack, Turbo Intruder is your go-to.
Press enter or click to view image in full size
I dove into another PortSwigger lab, this time a ‘Practitioner’ level challenge: ”Bypassing rate limits via race conditions.” The goal was clear: brute-force the password for the user carlos on a login mechanism protected by rate limiting, then log in and delete his account. The credentials for my own test account were wiener:peter.
The Challenge: The login mechanism would temporarily block an account after more than three incorrect password attempts. This meant a standard brute-force attack would be useless.
Step 1: Predicting a Potential Collision (Understanding the Rate Limit)
My first step was to understand how the rate limit worked. I intentionally submitted incorrect passwords for my wiener account.
After three incorrect attempts, the application temporarily blocked my account.
I tried logging in with another arbitrary username and observed the normal ‘Invalid username or password’ message. This confirmed the rate limit was enforced per-username, not per-session.
This was a crucial clue. The server had to be storing a failed attempt counter for each username. This meant there was a race window:
Between when I submitted a login attempt.
And when the website incremented the failed attempt counter for that username.
If I could send multiple login attempts for carlos within this tiny window, I might bypass the three-attempt limit.
Press enter or click to view image in full size
Step 2: Benchmarking the Behavior (Sequential Requests)
To get a baseline, I found a POST /login request (for an unsuccessful login) in Burp Proxy history and sent it to Repeater. I then duplicated this tab 19 times, creating a group of 20 identical requests. I set these to send in sequence.
As expected, after two more failed login attempts (making a total of three), my account was temporarily locked out. This confirmed the rate limit was indeed active and working as intended under normal, sequential conditions.
Step 3: Probing for Clues (Parallel Requests)
Now for the race. I sent the same group of 20 requests in parallel using Repeater. My goal was to overwhelm the server and hit it with many attempts before it could update the failed login counter.
I studied the responses. Although the account eventually locked, I noticed that more than three requests received the normal ‘Invalid username and password’ response before the lockout message appeared. This was the proof! If I was quick enough, I could submit more than three login attempts before the account lock was triggered.
Press enter or click to view image in full size
Step 4: Proving the Concept (Brute-forcing with Turbo Intruder)
With the race window confirmed, it was time to automate the brute-force. I sent one of the POST /login requests to Turbo Intruder.
1. In Turbo Intruder, the password parameter was automatically marked as a payload position (`%s`).
2. I changed the `username` parameter in the request to `carlos`.
3. From the dropdown menu, I selected the `examples/race-single-packet-attack.py` template. This template is designed for exactly this kind of HTTP/2 single-packet race attack.
4. I copied the provided list of candidate passwords (123123, abc123, football, etc.) to my clipboard.
5. I edited the Python script in Turbo Intruder to queue a login request using each password from the `wordlists.clipboard`.
Press enter or click to view image in full size
I launched the attack. Turbo Intruder sent all the password guesses in parallel, exploiting the race condition. I studied the responses carefully. If no successful logins appeared, I’d wait for the account lock to reset and repeat the attack, perhaps refining the password list.
Eventually, one of the responses came back with a `302 Found` status, indicating a successful login! I noted the corresponding password from the Payload column.
Press enter or click to view image in full size
Step 5: Accessing Admin Panel and Solving the Lab
After the account lock reset (if it was triggered), I logged in as carlos using the identified password. I then accessed the admin panel and deleted the user carlos to solve the lab.
Press enter or click to view image in full size
If you have any questions or require further clarification, don’t hesitate to reach out. Additionally, you can stay connected for more advanced cybersecurity insights and updates:
🔹 GitHub: @0xEhab
🔹 Instagram: @pjo_
🔹 LinkedIn: https://www.linkedin.com/in/ehxb/
Stay tuned for more comprehensive write-ups and tutorials to deepen your cybersecurity expertise. 🚀