Python pickle

Python's pickle module is unsafe by design for untrusted input: the format is a small stack language that the unpickler executes, and an object can declare, via __reduce__, a callable and arguments to run at load time. Unlike PHP or Java, there is no gadget-chain hunt; pickle.loads() on attacker bytes is direct, immediate code execution.

The reduce primitive#

__reduce__ returns a callable and a tuple of arguments; the unpickler calls it during reconstruction. A payload class names os.system (or subprocess, builtins.eval, builtins.exec) as the callable:

python
import pickle, os, base64

class RCE:
    def __reduce__(self):
        return (os.system, ('id',))

payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())

Anything that then calls pickle.loads(base64.b64decode(payload)) runs id. Common sinks: session or cache blobs, Authorization/cookie values, message-queue payloads, ML model files, and any API that accepts a pickled object. The same applies to cPickle, shelve, dill, and joblib, which wrap pickle.

Recognize pickle on the wire by its opcodes and trailing . (protocol 0 is ASCII-ish; higher protocols are binary and often base64-encoded).

Other Python sinks#

  • yaml.load (PyYAML) without a safe loader constructs arbitrary Python objects via the !!python/object/apply tag:

    yaml
    !!python/object/apply:os.system ["id"]
    

    yaml.load(data) executes this under yaml.Loader/UnsafeLoader (and the pre-5.1 default yaml.load with no loader). FullLoader was specifically designed to reject arbitrary callable application and does not run it by default; it was only exploitable through specific historical bypasses that current PyYAML has patched. yaml.safe_load never runs it. Test both the sink and the exact loader in use.

  • jsonpickle.decode reconstructs Python objects from JSON that carries py/object and py/reduce keys, giving the same __reduce__ primitive through a JSON-looking payload.

  • numpy.load(allow_pickle=True), pandas.read_pickle, joblib.load, and torch.load deserialize with pickle under the hood, so loading an untrusted .npy/.pkl/model file is the same primitive.

Exploitation notes#

  • Keep the payload's imports minimal and present on the target (os, subprocess, builtins); for output, use subprocess.check_output and exfiltrate, or go for a reverse shell since os.system output is not returned to you.
  • For restricted environments that block os, builtins.eval/exec with a constructed string reaches the same place.
  • pickle executes before any application code inspects the object, so input validation after loads() is too late and irrelevant to exploitation.

Tools#

  • Hand-crafted __reduce__ payloads; pker for assembling complex pickle opcodes.

References#

  • Python manual: pickle (security warning)
  • PortSwigger Web Security Academy: Insecure deserialization

Cookie Consent

We use cookies to enhance your experience. Learn more