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:
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/applytag:!!python/object/apply:os.system ["id"]yaml.load(data)executes this underyaml.Loader/UnsafeLoader(and the pre-5.1 defaultyaml.loadwith no loader).FullLoaderwas 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_loadnever runs it. Test both the sink and the exact loader in use. -
jsonpickle.decodereconstructs Python objects from JSON that carriespy/objectandpy/reducekeys, giving the same__reduce__primitive through a JSON-looking payload. -
numpy.load(allow_pickle=True),pandas.read_pickle,joblib.load, andtorch.loaddeserialize withpickleunder 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, usesubprocess.check_outputand exfiltrate, or go for a reverse shell sinceos.systemoutput is not returned to you. - For restricted environments that block
os,builtins.eval/execwith a constructed string reaches the same place. pickleexecutes before any application code inspects the object, so input validation afterloads()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