| --- |
| license: mit |
| tags: |
| - security-research |
| - modelscan-bypass |
| --- |
| |
| # Modelscan EOP Bypass PoC — STACK_GLOBAL Offset 0 Scanner Crash |
| |
| ## Vulnerability |
| |
| `picklescanner.py:87` uses `range(1, n)` to scan backward from STACK_GLOBAL opcode, |
| which **skips index 0**. Placing a callable's module argument at position 0 causes |
| the scanner to find only 1 argument instead of 2, raising `ValueError`. |
|
|
| This `ValueError` propagates to `modelscan.py:175` where a broad `except Exception` |
| handler converts it to a scanner **error** (not an **issue**). |
|
|
| **Result:** `modelscan` reports **"No issues found!"** while the pickle contains |
| `os.system` (a CRITICAL blocklisted callable) and executes arbitrary commands. |
|
|
| ## Impact |
|
|
| This bypasses the **entire** `unsafe_globals` blocklist. Any callable |
| (`os.system`, `subprocess.Popen`, etc.) can be used because the scanner |
| crashes before checking the blocklist. |
|
|
| ## Reproduction |
|
|
| ```bash |
| modelscan scan -p eop_bypass_poc.pkl |
| # Output: "No issues found! 🎉" |
| |
| python3 -c "import pickle; pickle.loads(open('eop_bypass_poc.pkl','rb').read())" |
| # Output: PWNED_BY_MODELSCAN_EOP_BYPASS |
| ``` |
|
|
| ## Root Cause |
|
|
| ```python |
| # picklescanner.py:86-87 |
| elif op_name == "STACK_GLOBAL": |
| values: List[str] = [] |
| for offset in range(1, n): # BUG: should be range(1, n+1) to include pos 0 |
| ``` |
|
|
| ## Fix |
|
|
| Change `range(1, n)` to `range(1, n+1)` in `picklescanner.py:87`. |
|
|