Class: Msf::Exploit::Remote::HTTP::RailsActiveStorageVips::PngDecoder

Inherits:
Object
  • Object
show all
Defined in:
lib/msf/core/exploit/remote/http/rails_active_storage_vips/png_decoder.rb

Overview

Applies strict resource limits before using ChunkyPNG to decode grayscale representation responses. ChunkyPNG receives only bounded, exactly sized scanlines because its unbounded parser must not process target-controlled PNG data. Format rules: www.w3.org/TR/png-3/

Constant Summary collapse

PNG_SIGNATURE =
ChunkyPNG::Datastream::SIGNATURE
MAX_PIXELS =
1_048_576
MAX_DATA_BYTES =
4_194_304
MAX_CHUNKS =
1024
MAX_IDAT_BYTES =
3_145_728

Class Method Summary collapse

Class Method Details

.decode(data) ⇒ Hash{Symbol => Integer, String}

Decodes a bounded, 8-bit, non-interlaced grayscale PNG image.

Parameters:

  • data (String)

    the binary PNG representation body

Returns:

  • (Hash{Symbol => Integer, String})

    the image dimensions, channel count, and grayscale pixels

Raises:

  • (TriggerError)

    if the body is malformed, unsupported, or exceeds a resource limit



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/msf/core/exploit/remote/http/rails_active_storage_vips/png_decoder.rb', line 22

def self.decode(data)
  raise TriggerError, 'Representation body is not a PNG image' unless data.is_a?(String) && data.start_with?(PNG_SIGNATURE)
  raise TriggerError, 'Representation PNG exceeded the maximum encoded size' if data.bytesize > MAX_DATA_BYTES

  offset = PNG_SIGNATURE.bytesize
  idat = String.new.b
  chunk_count = 0
  width = nil
  height = nil
  bit_depth = nil
  color_type = nil
  compression = nil
  filter_method = nil
  interlace = nil
  saw_iend = false

  while offset + 12 <= data.bytesize
    chunk_count += 1
    raise TriggerError, 'Representation PNG contained too many chunks' if chunk_count > MAX_CHUNKS

    length = data.byteslice(offset, 4).unpack1('N')
    raise TriggerError, 'Representation PNG contains an oversized chunk' if length > data.bytesize - offset - 12

    chunk_type = data.byteslice(offset + 4, 4)
    chunk_data = data.byteslice(offset + 8, length)
    chunk_crc = data.byteslice(offset + 8 + length, 4).unpack1('N')
    expected_crc = Zlib.crc32(chunk_type + chunk_data)
    raise TriggerError, "Representation PNG #{chunk_type.inspect} chunk failed its CRC check" unless chunk_crc == expected_crc

    case chunk_type
    when 'IHDR'
      raise TriggerError, 'Representation PNG contained an invalid IHDR chunk' unless length == 13 && width.nil?

      width, height, bit_depth, color_type, compression, filter_method, interlace = chunk_data.unpack('NNC5')
    when 'IDAT'
      raise TriggerError, 'Representation PNG image data exceeded the compressed size limit' if idat.bytesize + length > MAX_IDAT_BYTES

      idat << chunk_data
    when 'IEND'
      saw_iend = true
      break
    end

    offset += 12 + length
  end

  raise TriggerError, 'Representation PNG is missing IHDR data' unless width && height
  raise TriggerError, 'Representation PNG dimensions were invalid' unless width.positive? && height.positive? && width * height <= MAX_PIXELS
  raise TriggerError, "Unsupported PNG bit depth #{bit_depth}" unless bit_depth == 8
  raise TriggerError, "Unsupported PNG color type #{color_type}" unless color_type == 0
  raise TriggerError, "Unsupported PNG compression method #{compression}" unless compression.zero?
  raise TriggerError, "Unsupported PNG filter method #{filter_method}" unless filter_method.zero?
  raise TriggerError, 'Interlaced PNG responses are not supported' unless interlace.zero?
  raise TriggerError, 'Representation PNG is missing image data' if idat.empty?
  raise TriggerError, 'Representation PNG is missing IEND data' unless saw_iend

  stride = width
  expected_size = height * (stride + 1)
  raw = inflate_limited(idat, expected_size)
  raise TriggerError, 'Representation PNG scanline data had an unexpected length' unless raw.bytesize == expected_size

  canvas = ChunkyPNG::Canvas.decode_png_pixelstream(
    raw,
    width,
    height,
    ChunkyPNG::COLOR_GRAYSCALE,
    bit_depth,
    ChunkyPNG::INTERLACING_NONE,
    nil,
    nil
  )

  { width: width, height: height, channels: 1, pixels: canvas.to_grayscale_stream }
rescue Zlib::Error => e
  raise TriggerError, "Representation PNG decompression failed: #{e.message}"
rescue ChunkyPNG::Exception => e
  raise TriggerError, "Representation PNG decoding failed: #{e.message}"
end