drogba771's picture
Upload README.md with huggingface_hub
8115caa verified
|
Raw
History Blame Contribute Delete
5.95 kB
# 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
```bash
# 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
```bash
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
```bash
modelscan scan -p selfcontained_bypass_277.pkl -r json
# Expected: "total_issues": 0
```
### Step 3: Reproduce from scratch
```bash
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