| # modelscan Bypass PoC: distutils.spawn.spawn() |
|
|
| ## Vulnerability |
|
|
| modelscan 0.7.6 fails to detect malicious pickle files that use `distutils.spawn.spawn()` for command execution. The `distutils.spawn` module is not included in modelscan's `unsafe_globals` blocklist. |
|
|
| ## Attack Chain |
|
|
| Minimal single-REDUCE chain: |
| 1. `STACK_GLOBAL "distutils.spawn" "spawn"` — loads the spawn function |
| 2. Build argument list `["sh", "-c", "arbitrary_command"]` |
| 3. `REDUCE` — calls `spawn(["sh", "-c", "arbitrary_command"])` |
|
|
| ## Impact |
|
|
| - **Full RCE** via `pickle.load()` or `torch.load()` |
| - Bypasses modelscan 0.7.6 completely — reports "No issues found" |
| - Extremely simple chain — single function call, no chaining needed |
| - `distutils.spawn.spawn()` internally calls `os.spawnv()` but this is invisible to modelscan's static opcode analysis |
|
|
| ## Reproduction |
|
|
| ```bash |
| python create_poc.py |
| modelscan scan -p pytorch_model.bin |
| # Output: "No issues found" |
| |
| python -c "import pickle; pickle.loads(open('pytorch_model.bin','rb').read())" |
| # Creates /tmp/distutils_bypass_proof |
| ``` |
|
|
| ## Root Cause |
|
|
| modelscan only checks top-level module references in pickle GLOBAL/STACK_GLOBAL opcodes against its blocklist. `distutils.spawn` is not listed. The internal delegation to `os.spawnv()` happens at Python runtime, not in pickle bytecode. |
| |
| ## Fix Recommendation |
| |
| Add `"distutils.spawn": "*"` to `unsafe_globals.CRITICAL` in `modelscan/settings.py`. |
|
|