--- license: mit tags: - security-poc - coordinated-disclosure --- # PoC: modelscan 0.8.8 multi-pickle legacy scan bypass (scanner evasion) > Coordinated-disclosure proof-of-concept submitted to huntr. The model file in this > repository is a **benign test artifact**: its payload only runs `touch MP_legacy_pwn` > (creates an empty marker file in the working directory). No network access, no > destructive action. Do not load untrusted model files. ## What this demonstrates `modelscan` 0.8.8 reports `evil_legacy.pt` as clean ("No issues found! 🎉", exit 0), yet `torch.load("evil_legacy.pt", weights_only=False)` executes the payload. The identical `os.system` operator in a single-stream pickle **is** flagged — so this is a scan-coverage correctness defect, not a denylist gap. This is a **scanner-evasion** finding (huntr "unique methods to bypass our automated scanners"), not a report of pickle deserialization itself. Root cause: `scan_pytorch()` (`modelscan/tools/picklescanner.py`) calls `scan_pickle_bytes(..., multiple_pickles=False)`, so `_list_globals()` breaks after the first of the file's sequential pickle streams and never inspects the model object graph. ## How the PoC model was created A well-formed 5-stream PyTorch legacy file: magic number, protocol, sys_info, the `os.system` payload object, and an empty storage-keys list (so `torch.load` returns without error — a valid file, not malformed). See `poc.py`. ## Files - `evil_legacy.pt` — the malicious-but-benign model file (modelscan-clean, executes on load). - `poc.py` — self-contained: builds the file, scans it (clean), loads it (executes), and runs a single-stream control (flagged). One command. ## Reproduce ``` pip install "modelscan==0.8.8" torch modelscan -p evil_legacy.pt # -> No issues found! 🎉 (exit 0) python -c "import torch; torch.load('evil_legacy.pt', weights_only=False)" # -> creates MP_legacy_pwn python poc.py # full differential, prints: RESULT: PASS ``` ## Affected / fix modelscan 0.8.8 (latest at disclosure). Fix: pass `multiple_pickles=True` in the PyTorch legacy scan path (`scan_pytorch`), matching the non-legacy behaviour.