Post

Web Application Race Conditions Vulnerabilities

This note covers race condition vulnerabilities in web applications, where multiple processes or requests are executed simultaneously, leading to unintended behavior. It includes labs on bypassing rate limits, exploiting time-sensitive flaws, multi- and single-endpoint issues, as well as references to bug bounty reports and Turbo Intruder scripts for practical exploitation.

Web Application Race Conditions Vulnerabilities

Labs from PortSwigger Academy

Lab 1: Limit overrun race conditions

consider an online store that lets you enter a promotional code during checkout to get a one-time discount on your order. To apply this discount, the application may perform the following high-level steps:

  1. Check that you haven’t already used this code.
  2. Apply the discount to the order total.
  3. Update the record in the database to reflect the fact that you’ve now used this code.

There are many variations of this kind of attack, including:

  • Redeeming a gift card multiple times
  • Rating a product multiple times
  • Withdrawing or transferring cash in excess of your account balance
  • Reusing a single CAPTCHA solution
  • Bypassing an anti-brute-force rate limit

Limit overruns are a subtype of so-called “time-of-check to time-of-use” (TOCTOU) flaws. Later in this topic, we’ll look at some examples of race condition vulnerabilities that don’t fall into either of these categories.

1
2
3
4
5
we have coupon: PROMO20 which can be used only once.

make 10 duplicates of request POST /car/coupon
then send requests as `Send group (Parallel)`

In Burp Pro

1
Inside repeater tab, there is Custom Action which contains Trigger race condition

Lab 2: Bypassing rate limits via race conditions

This lab’s login mechanism uses rate limiting to defend against brute-force attacks. However, this can be bypassed due to a race condition.

To solve the lab:

  1. Work out how to exploit the race condition to bypass the rate limit.
  2. Successfully brute-force the password for the user carlos.
  3. Log in and access the admin panel.
  4. Delete the user carlos.

You can log in to your account with the following credentials: wiener:peter. Password wordlist is provided.

1
2
3
4
5
6
7
8
9
10
POST /login

username=carlos&password=superman

if we try to bruteforce then we get `You have made too many incorrect login attempt. Please try again in .. seconds`

we can bypass this protection by using race condition (by grouping the request and send group parallel)
next challange is how to provide different password for different request. For this we use Turbo Intruder extension


we use edited version of turbo intruder script examples/race-single-packet-attack.py copy the wordlist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def queueRequests(target, wordlists):

    # if the target supports HTTP/2, use engine=Engine.BURP2 to trigger the single-packet attack
    # if they only support HTTP/1, use Engine.THREADED or Engine.BURP instead
    # for more information, check out https://portswigger.net/research/smashing-the-state-machine
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2
                           )

    # the 'gate' argument withholds part of each request until openGate is invoked
    # if you see a negative timestamp, the server responded before the request was complete
    for i in range(20):
        engine.queue(target.req, gate='race1')

    # once every 'race1' tagged request has been queued
    # invoke engine.openGate() to send them in sync
    engine.openGate('race1')


def handleResponse(req, interesting):
    table.add(req)

Note: make sure to set password value as %s

Lab 3: Multi-endpoint race conditions

Steps

Predict a potential collision

  • POST /cart–> adds item to the cart
  • POST /cart/checkout –> submits your order.
  • The status of cart is stored based on session ID
    • this indicated there is potential for a collision (because) if the cart state is looked up by one key (session ID), then two requests presenting the same key will operate on the same cart — that’s a collision.
  • Another key thing: Submitting and receiving confirmation of a successful order takes place over a single request/response cycle, which creates opportunity for race condition.
  • Consider that there may be a race window between when your order is validated and when it is confirmed. This could enable you to add more items to the order after the server checks whether you have enough store credit.

Attack

  • If any requests are send one by one then it takes around 700-800 milliseconds as it need to verify the cookie.
  • But if we do 2 requests simultaneously then 1st request takes 700-800 milliseconds and later requests takes much smaller time.
  • Therefore, we add following in one group
    • GET / –> to warmup
    • POST /cart –> to submit order of item which is costly
    • POST /cart/checkout –> to submit the order.
  • NOTE: Make sure to add any product that we can purchase in cart. (In our case, gift card)

Then we use Repeater’s Send group (Parallel), here is what it does.

  • GET / –> to warmup’s the request so that other request time decreases
  • POST /cart –> this add expensive product to the cart
  • POST /cart/checkout –> This checkout the gift card we added.
  • Since we are using Parallel request, our expensive product is added to the cart and checkout happens at same time. Hence we get the expensive product for free

How an attacker exploits it (interleaving example):

  1. Attacker sends POST /cart/checkout → server starts validation (reads cart: only cheap item).
  2. Before the server finishes commit, attacker (or another request the attacker triggers) sends POST /cart to add an expensive item.
  3. If the server commits the order after that change but used the earlier validation result (or the commit used the now-changed cart without re-checking funds), the expensive item can be included despite insufficient funds.

Lab 4: Single-endpoint race conditions

This lab’s email change feature contains a race condition that enables you to associate an arbitrary email address with your account.

Someone with the address [email protected] has a pending invite to be an administrator for the site, but they have not yet created an account. Therefore, any user who successfully claims this address will automatically inherit admin privileges.

To solve the lab:

  1. Identify a race condition that lets you claim an arbitrary email address.
  2. Change your email address to [email protected].
  3. Access the admin panel.
  4. Delete the user carlos

You can log in to your own account with the following credentials: wiener:peter.

You also have access to an email client, where you can view all emails sent to @exploit-<YOUR-EXPLOIT-SERVER-ID>.exploit-server.net addresses.

Attack

The application only stores one pending email change per account. Submitting a new email overwrites any previous pending email, rather than queuing multiple requests. This creates a race condition: if multiple POST /my-account/change-email requests are sent in parallel with different email addresses, the database entry can be updated unpredictably while the server is generating confirmation emails.

As a result, confirmation emails may be sent to the wrong address, allowing an attacker to confirm an email they did not originally request. This can lead to unauthorized account access if exploited properly.

Steps to reproduce:

  1. Send two parallel requests to change your account email — one to an email you control, one to the target’s email.
  2. Observe which email receives the confirmation link; due to the race, the confirmation link may correspond to the target’s email.
  3. Confirm the change using the link to gain access to the victim account (lab example: access admin panel).

Group two POST /my-account/change-email requests:

  • In the first request, use your own valid email.
  • In the second request, use [email protected]. Send both requests in parallel.

Result:

  • Due to the race condition, confirmation emails may be sent to the wrong address.
  • In this case, the confirmation intended for [email protected] is sent to your email, allowing you to confirm the change and gain admin access.

Lab 5: Exploiting time-sensitive vulnerabilities

This lab contains a password reset mechanism. Although it doesn’t contain a race condition, you can exploit the mechanism’s broken cryptography by sending carefully timed requests.

To solve the lab:

  1. Identify the vulnerability in the way the website generates password reset tokens.
  2. Obtain a valid password reset token for the user carlos.
  3. Log in as carlos.
  4. Access the admin panel and delete the user carlos.

You can log into your account with the following credentials: wiener:peter.


Consider a password reset token that is only randomized using a timestamp. In this case, it might be possible to trigger two password resets for two different users, which both use the same token. All you need to do is time the requests so that they generate the same timestamp.

  • POST /forgot-password with data username=wiener –> sends password reset URL to wiener’s mail
  • if we send two request at same time (parallel), we get same password reset URL . Note: we need to use different sessionid and csrf token
  • After getting this conclusion, we change the username of 2nd request to target user carlos
  • Then editing wiener’s password URL, we can reset password of carlos.

Some Bug Bounty Reports

Turbo Intruder Scripts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def queueRequests(target, wordlists):

    # if the target supports HTTP/2, use engine=Engine.BURP2 to trigger the single-packet attack
    # if they only support HTTP/1, use Engine.THREADED or Engine.BURP instead
    # for more information, check out https://portswigger.net/research/smashing-the-state-machine
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2
                           )

    engine.queue(target.req, '[email protected]')
    time.sleep(0.5)

    engine.queue(target.req, '[email protected]', gate='race1')
    engine.queue(target.req, '[email protected]', gate='race1')
    engine.openGate('race1')


def handleResponse(req, interesting):
    table.add(req)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def queueRequests(target, wordlists):

    # as the target supports HTTP/2, use engine=Engine.BURP2 and concurrentConnections=1 for a single-packet attack
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2
                           )
    
    # assign the list of candidate passwords from your clipboard
    passwords = wordlists.clipboard
    
    # queue a login request using each password from the wordlist
    # the 'gate' argument withholds the final part of each request until engine.openGate() is invoked
    for password in passwords:
        engine.queue(target.req, password, gate='1')
    
    # once every request has been queued
    # invoke engine.openGate() to send all requests in the given gate simultaneously
    engine.openGate('1')


def handleResponse(req, interesting):
    table.add(req)
This post is licensed under CC BY 4.0 by the author.

© dollarboysushil. Some rights reserved.

Using the Chirpy theme for Jekyll.