SmartHire Writeup - HackTheBox
Created May 16, 2026 • Last updated October 11, 2026 • 1 min read
1. Reconnaissance
Fast TCP Port Scan
Start with a fast scan to identify open ports quickly.
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-17 00:13 +0500 Nmap scan report for smarthire.htb (10.129.26.203) Host is up (0.21s latency). Not shown: 991 closed tcp ports (reset), 7 filtered tcp ports (no-response) Some closed ports may be reported as filtered due to --defeat-rst-ratelimit PORT STATE SERVICE 22/tcp open ssh 80/tcp open http Nmap done: 1 IP address (1 host up) scanned in 4.05 seconds
Service & Version Detection
Run a detailed scan with service detection and default scripts against the open ports.
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-17 00:15 +0500 Nmap scan report for smarthire.htb (<Target_Ip>) Host is up (0.25s latency). PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 41:3c:e3:bb:88:70:99:7f:b8:96:59:48:9b:85:98:69 (ECDSA) |_ 256 d5:9d:fd:6b:be:d8:39:6f:3f:43:ab:0e:f6:3e:22:db (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Overview | SmartHIRE |_http-server-header: nginx/1.18.0 (Ubuntu) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Add Hostname to /etc/hosts
<Target_Ip> smarthire.htb
2. Enumeration
Subdomain Fuzzing
Fuzz for hidden subdomains using ffuf.
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://FUZZ.smarthire.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.smarthire.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 0
________________________________________________
models [Status: 401, Size: 137, Words: 11, Lines: 1, Duration: 213ms]
:: Progress: [4989/4989] :: Job [1/1] :: 45 req/sec :: Duration: [0:00:17] :: Errors: 4988 ::
A subdomain
modelsis discovered. Add it to/etc/hosts.
<Target_Ip> models.smarthire.htb
3. Initial Access , CVE-2024-37054 (MLflow RCE)
Navigate to http://smarthire.htb/, click signup, and create an account.
Step 1: Login to SmartHire
Step 2: Generate malicious pickle
Step 3: Create train.csv
Before uploading, create the train.csv file with the following content:
Command:
Step 4: Upload training CSV to register new model version
{"message":"Model trained and registered successfully","model_deleted":false,"model_info":{"creation_timestamp":1778966750869,"description":"No description","version":"4"},"registered_model":"benslates-984cec44e985-model","status":"success"}
Step 5: Fetch run_id from MLflow API
1b657efe71844222b87865544906197c
Step 6: Overwrite python_model.pkl with malicious pickle
Replace <run_id> with the ID obtained in the previous step.
Step 7: Trigger prediction and catch shell
Terminal 1 , Netcat listener:
Terminal 2 , Trigger prediction:
Netcat Listener Output:
listening on [any] 4444 ...
connect to [<tun0_ip>] from (UNKNOWN) [<Target_Ip>] 50450
bash: cannot set terminal process group (1022): Inappropriate ioctl for device
bash: no job control in this shell
svcweb@smarthire:/var/www/smarthire.htb$
5. Privilege Escalation , Python .pth Abuse
Check Sudo Permissions
User svcweb may run the following commands on smarthire:
(root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
Inspect the Script Source Code
#!/usr/bin/env python3
from pathlib import Path
import sys
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))
def print_usage():
print("Usage: mlflowctl.py [status|backup-models|restart]")
sys.exit(1)
def main():
import mlflow_actions, backup_models
if len(sys.argv) < 2:
print_usage()
action = sys.argv[1]
if action == "status":
mlflow_actions.check_status()
elif action == "backup-models":
print("[*] Running backup via backup_models plugin...")
backup_models.run()
elif action == "restart":
mlflow_actions.restart()
else:
print(f"[!] Unknown action: {action}")
print_usage()
if __name__ == "__main__":
main()
Inspect Writable Plugins Directory
The script imports plugins from /opt/tools/mlflow_ctl/plugins/.
drwxr-xr-x 4 root root 4096 Feb 19 18:10 . drwxr-xr-x 3 root root 4096 Feb 19 18:16 .. drwxr-xr-x 3 root root 4096 Feb 20 09:26 core drwxrwxr-x 2 root devs 4096 May 12 15:22 dev
The
devdirectory is writable by thedevsgroup, whichsvcwebbelongs to.
Trigger Root Shell
root@smarthire:/# id uid=0(root) gid=0(root) groups=0(root)