YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

ModelScan 0.8.8 Bypass β€” Pickle Deserialization RCE

Target: huntr.com (Protect AI Model Upload Program)

Reward Tier: $750 (Unsafe Deserialization β€” Scanner Bypass)

CWE: CWE-502 (Deserialization of Untrusted Data)


Executive Summary

ModelScan 0.8.8 is Protect AI's scanner for detecting unsafe operations in ML model files. It flags pickle files containing dangerous GLOBAL imports (e.g., os.system, subprocess.Popen) by analyzing pickle opcodes via pickletools.genops().

This report demonstrates a complete bypass of ModelScan 0.8.8's pickle scanner. The technique achieves arbitrary code execution via pickle.load() while producing zero ModelScan issues.

Status: βœ… ModelScan bypass confirmed | βœ… RCE confirmed


The Vulnerability

Python's pickle module is inherently unsafe β€” pickle.load() can execute arbitrary code by design. ModelScan attempts to detect this by scanning pickle opcodes for "unsafe" module imports.

Blocklist (from modelscan/settings.py)

Severity Blocked Modules/Functions
CRITICAL __builtin__.{eval,compile,getattr,apply,exec,open,breakpoint,__import__}, builtins.{eval,...}, os*, nt*, posix*, subprocess*, sys*, socket*, pty*, pickle*, _pickle*, shutil*, asyncio*, runpy*, bdb*, pdb*, operator.attrgetter
HIGH webbrowser*, httplib*, requests.api*, aiohttp.client*

Critical Gap: Modules NOT in the Blocklist

The following stdlib modules can be used for code execution but are completely absent from ModelScan's blocklist:

  • βœ… marshal β€” marshal.loads() deserializes Python code objects
  • βœ… types β€” types.FunctionType() creates executable functions from code objects
  • βœ… ctypes β€” ctypes.CDLL(None).system() calls libc directly
  • βœ… importlib β€” importlib.import_module('os') loads any module at runtime

Golden PoC: selfcontained_bypass_277.pkl

Metric Value
Size 277 bytes
SHA256 9bc06071f234c0ddfe96ca9b43ecd873516492cbf99ae2ab835fbbbf2a6a2f80
pickle.load() RCE βœ… Confirmed
ModelScan 0.8.8 βœ… 0 issues (0 critical, 0 high, 0 medium, 0 low)
Dependencies None β€” fully self-contained

Verification

# RCE test (no dependencies)
python3.11 -c "import pickle; pickle.load(open('selfcontained_bypass_277.pkl','rb'))"
cat /tmp/huntr_bypass_poc  # β†’ BYPASS_OK

# ModelScan bypass verification
modelscan scan -p selfcontained_bypass_277.pkl -r json
# β†’ "total_issues": 0, "issues": []

Tested environment: CPython 3.11.15 + ModelScan 0.8.8 Note: marshal-based code objects are CPython minor-version specific; regenerate using build_selfcontained_pickle.py for the target Python minor version.

Technical Breakdown

The pickle uses only 2 GLOBAL imports, neither blocked:

Opcode Import In ModelScan blocklist?
GLOBAL types FunctionType types.FunctionType ❌ NOT BLOCKED
GLOBAL marshal loads marshal.loads ❌ NOT BLOCKED

Execution chain:

pickle.load()
  β†’ marshal.loads(embedded_code_bytes)   # NOT scanned
  β†’ types.FunctionType(code_obj, {})     # NOT scanned  
  β†’ function() β†’ import os β†’ os.system() # Runtime, invisible to scanner

The embedded code object (232 bytes) contains a compiled function that imports os and calls os.system() at runtime β€” ModelScan never sees these opcodes because they don't exist in the pickle stream.

Alternative Bypass Variants

All confirmed 0 issues on ModelScan 0.8.8:

Variant Size Technique
marshal+types 291 B marshal.loads β†’ types.FunctionType
ctypes 115 B ctypes.CDLL(None).system()
importlib 118 B importlib.import_module('os')
class_traversal 124 B ().__class__.__bases__[0].__subclasses__() β†’ os.system

Reproduction

Requirements

  • Python 3.8+
  • modelscan>=0.8.0 (optional, for verification)
  • No additional dependencies

Step 1: Verify RCE

python3.11 -c "import pickle; pickle.load(open('selfcontained_bypass_277.pkl','rb'))"
cat /tmp/huntr_bypass_poc  # β†’ BYPASS_OK

Step 2: Verify ModelScan bypass

modelscan scan -p selfcontained_bypass_277.pkl -r json
# Expected: "total_issues": 0

Step 3: Reproduce from scratch

python3.11 build_selfcontained_pickle.py
# Builds the 277-byte payload from source code (CPython version-specific)

Deliverable Files

File Size Description
selfcontained_bypass_277.pkl 277 B ⭐ Golden PoC β€” self-contained pickle, 0 deps, ModelScan bypass
build_selfcontained_pickle.py 7.5 KB Builder script (reproduces golden PoC)
README.md This file
pickles/pickle_bypass_marshal.pkl 291 B marshal+types variant
pickles/pickle_bypass_ctypes.pkl 115 B ctypes variant
pickles/pickle_bypass_importlib.pkl 118 B importlib variant
pickles/pickle_bypass_class_traversal.pkl 124 B class_traversal variant

Impact

  • Any application using pickle.load(), joblib.load(), or torch.load() on untrusted files is vulnerable
  • ModelScan 0.8.8 provides zero protection against this attack
  • The technique works against ALL pickle-compatible scanners that only check GLOBAL opcodes
  • Alternate bypasses: ctypes, importlib, builtins (partially)

Mitigation

  • Do NOT rely on static pickle analysis for security β€” pickle is inherently unsafe
  • Use alternative formats: .safetensors, ONNX, or signed pickles
  • Run model loading in isolated sandboxes (containers, seccomp, AppArmor)
  • Add marshal, types, ctypes, importlib to ModelScan's blocklist
  • Implement behavioral analysis or dynamic taint tracking
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support