π AI Security Research: PyTorch Pickle Deserialization PoC (harmless payload) π
Purpose of this repository
This is an educational security research artifact, not a functional model. It was built as part of hands-on AI Red Teaming coursework (Hack The Box Academy) to demonstrate, end-to-end, a well-known and already publicly documented class of vulnerability: arbitrary code execution via unsafe pickle deserialization of ML checkpoints. Hugging Face's own security docs describe this exact mechanism β see "Pickle Scanning" β and this repo is a hands-on, disclosed reproduction of that documented risk, built for learning and awareness rather than for use against any target.
Video demo: unedited screen recording of loading this exact checkpoint and watching the payload fire.
To keep the demonstration unambiguous and safe to publish, the embedded "payload" carries zero harmful capability by design: it opens the default web browser to a YouTube video (Rick Astley's "Never Gonna Give You Up"). No network callback, no filesystem access, no credential access, no persistence, no shell. The vulnerability class demonstrated is real and serious; the payload used to illustrate it deliberately is not.
Who this is for: ML engineers, MLOps/platform teams, and security
practitioners who want to see β concretely and safely β what "just load the
checkpoint" can mean on an untrusted file, so they can make informed decisions
about weights_only, safetensors, and model provenance in their own
pipelines.
Does torch.load() trigger this automatically?
Not by default, and that matters. Since PyTorch 2.6, torch.load()
defaults to weights_only=True, which uses a restricted unpickler and blocks
exactly this class of attack. Loading this file with a modern, unmodified
torch.load(path) will raise an UnpicklingError and refuse to execute the
payload β verified against PyTorch 2.11.
The payload does fire if the checkpoint is loaded via:
torch.load(path, weights_only=False)β often used to "get an old checkpoint to load" or because the loading code predates PyTorch 2.6.- Plain
pickle.load(open(path, "rb"))β has never had this restriction and is still fully unsafe. - Any older PyTorch version (< 2.6) using default settings.
In other words: the platform-level fix exists and works, but it's opt-out, not
universal. A huge amount of real-world code still explicitly sets
weights_only=False, or uses pickle directly, or predates the change.
What this model actually is
Underneath the trap, rickroll_trojan_model.pth wraps a small, legitimately
trained SimpleNet (a couple of nn.Linear layers). It has no real-world use
as a model β its only purpose is to be loaded.
How the attack works
- Steganographic payload hiding. A short Python snippet (
webbrowser.open(url)) is embedded into the least significant bits (LSBs) of one of the model's weight tensors, 1β2 bits per float32 value. The perturbation is small enough to be statistically invisible β it does not show up as an anomalous file size, hash mismatch, or obvious weight distribution shift. - Pickle
__reduce__hijack. The object actually saved to disk is not a plainstate_dict, it's a wrapper class whose__reduce__method returns(exec, (loader_code,)). Python'spicklemodule calls this automatically during unsafe deserialization, which means the loader code runs as a side effect of unpickling, not of any model call. - Trigger. The loader code reconstructs the real
state_dict, extracts the poisoned tensor, decodes the hidden payload from its LSBs, and executes it viaexec(). - Result.
torch.load("rickroll_trojan_model.pth", weights_only=False)(or plainpickle.load) is sufficient to trigger execution. Nomodel.eval(), no inference call, no user click beyond loading the file.
This is the exact same mechanism used by real-world malicious model attacks
(e.g., reverse shells, credential stealers, cryptominers hidden in .pth/.pkl
files uploaded to public model hubs) β the only thing swapped out here is the
payload.
How to inspect this file safely (i.e., without running it)
Never load an untrusted checkpoint with weights_only=False or plain
pickle.load(). To verify what a .pth/.pkl file actually does first:
- Static opcode inspection: use
ficklingorpicklescanto disassemble the pickle stream and flag dangerous opcodes (GLOBAL,REDUCE, calls toexec,os.system,subprocess, etc.) without executing anything. - Prefer safe formats:
safetensorsstores only tensor data, no executable pickle opcodes, and cannot do this by construction. If you don't control the source of a checkpoint, insist on.safetensors. - Leave
weights_onlyat its default (True) on PyTorch >= 2.6. This file is a good test case: loading it this way should fail with anUnpicklingErrorrather than execute anything. - Sandbox first: if you must load an unfamiliar checkpoint with
weights_only=Falsefrom a trusted-but-unverified source, do it in an isolated VM/container with no network access and no sensitive credentials mounted.
Why this matters
Public model hubs (including this one) work on trust: anyone can upload a
.pth file, and depending on how it's loaded, that can be equivalent to
running arbitrary code on your machine. weights_only=True closes the default
path, but plenty of real pipelines still opt out of it, and pickle.load()
was never protected. This model is a controlled, disclosed, harmless
demonstration of that gap, so that teams can see the failure mode before they
encounter it with a payload that isn't a YouTube link.
Intent & responsible use
This checkpoint is published openly and with full disclosure as part of
AI Red Teaming study material. The embedded payload is intentionally
non-destructive, makes no network connections, exfiltrates nothing, and leaves
no persistence. It is safe to trigger deliberately (e.g., in a VM or throwaway
environment, explicitly passing weights_only=False) to see the technique in
action, but it should not be loaded that way inside any pipeline, notebook, or
environment holding real credentials or sensitive data β the point is exactly
that you cannot tell from the file itself that this is happening.
Do not strip this model card, rename the file to obscure its origin, or re-upload the payload with anything other than a harmless action. Repurposing this technique against a target without authorization is illegal in most jurisdictions (unauthorized access / computer misuse statutes) β this artifact is for authorized security research, red teaming, and education only.
Repro / source
Built with PyTorch, LSB steganography over float32 weight tensors, and a
pickle.__reduce__ loader. Full notebook walkthrough (training the base
model, encoding the payload, building the malicious wrapper) is available here:
https://github.com/Draichi/htb/blob/main/data_attacks/steganography_rickroll_attack.ipynb