Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2023-39361 — Step-by-step analysis and lab setup for CVE-2023-39361, an unauthenticated SQL injection in Cacti v1.2.24, with exploitation and mitigation walkthrough. | Kitploit
Tools/GitHubGitHub/hpt-intern-task-submission/cve-2023-39361
Vulnerability AnalysisExploitationWeb Application ExploitationLearning & EducationLabs & Practice
GitHubhpt-intern-task-submission/cve-2023-39361

CVE-2023-39361

Step-by-step analysis and lab setup for CVE-2023-39361, an unauthenticated SQL injection in Cacti v1.2.24, with exploitation and mitigation walkthrough.

View Repository
102 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

[CVE-2023-39361] Unauthenticated SQL injection in Cacti v1.2.24

Overview

Cacti is an open-source operational monitoring tool written in PHP, MySQL/MariaDB, which provides a friendly interface. The vulnerability was found in 2023 which affected all versions before 1.2.24. This security flaws lies in the improper implementation when inserting values into SQL query. This is a critical SQL Injection vulnerability that allow attackers to modify database as well as execution code remotely.

Lab Setup:

In this analysis, I will run Cacti in Docker for simplicity. First, we will create the file docker-compose.yml as below and run command docker-compose up -d.

version: '3.5'

services:

  
  

cacti:

image: "smcline06/cacti"

container_name: CVE-2023-39361

domainname: example.com

hostname: cacti

ports:

- "80:80"

- "443:443"

environment:

- DB_NAME=cacti_master

- DB_USER=cactiuser

- DB_PASS=cactipassword

- DB_HOST=db

- DB_PORT=3306

- DB_ROOT_PASS=rootpassword

- INITIALIZE_DB=1

- TZ=America/Los_Angeles

volumes:

- cacti-data:/cacti

- cacti-spine:/spine

- cacti-backups:/backups

links:

- db

  
  

db:

image: "mariadb:10.3"

container_name: CVE-2023-39361_db

domainname: example.com

hostname: db

ports:

- "3306:3306"

command:

- mysqld

- --character-set-server=utf8mb4

- --collation-server=utf8mb4_unicode_ci

- --max_connections=200

- --max_heap_table_size=128M

- --max_allowed_packet=32M

- --tmp_table_size=128M

- --join_buffer_size=128M

- --innodb_buffer_pool_size=1G

- --innodb_doublewrite=ON

- --innodb_flush_log_at_timeout=3

- --innodb_read_io_threads=32

- --innodb_write_io_threads=16

- --innodb_buffer_pool_instances=9

- --innodb_file_format=Barracuda

- --innodb_large_prefix=1

- --innodb_io_capacity=5000

- --innodb_io_capacity_max=10000

environment:

- MYSQL_ROOT_PASSWORD=rootpassword

- TZ=America/Los_Angeles

volumes:

- cacti-db:/var/lib/mysql

  
  

volumes:

cacti-db:

cacti-data:

cacti-spine:

cacti-backups:

After building the container, we can access to Cacti by browsing to http://localhost:80. The next step is to update Cactii to version 1.2.24. You can download the upgrade script here: https://pastebin.com/NfRiHLjR. Save the file at the same directory with the docker-compose.yml file and rename it to upgrade_cacti.sh. Then run these 2 commands:

docker cp upgrade_cacti.sh CVE-2023-39361:/tmp/upgrade_cacti.sh && docker exec -it CVE-2023-39361 bash -c "bash /tmp/upgrade_cacti.sh"

Now we're good to go, let's dive into the code to see what happened that allows us to have a SQL injection attack. The vulnerable file is graph_view.php and we only need guest user to access to this file, leading to allow everyone to exploit this vulnerability. In this file, the insecure function is grow_right_pane_tree().

grow_right_pane_tree

Before, we dive deeply into this function, we need to trace backwards to know how the functions is executed.

<?php
switch (get_nfilter_request_var('action')) {
//.... 
// Many other cases
case  'tree_content':
	//..... Some code here
	// ---------
	if (isset_request_var('node')) {
		$parts = explode('-', sanitize_search_string(get_request_var('node')));
		// Check for tree anchor
		if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
			$tree_id = $parts[1];
			$node_id = 0;
		} 
	//..... Some code here

	if ($tree_id > 0) {
		if (!is_tree_allowed($tree_id)) {
			header('Location: permission_denied.php');
			exit;
		}
		// Vulnerable function is called here
		grow_right_pane_tree($tree_id, $node_id, $hgdata);
	}

From above code snippet, we can see that the function will be executed if $tree_id > 0. All the step can be explained as below:

  • First, the program will take the value of action parameter from user's input and step in a switch/case statement.

  • In the case tree_content, the code will take the request parameter node from user. Next, it will split the input by the - character, save to $part and finally check if tree_anchor appears in node. If all conditions match, $tree_id will be assign as the second element in $part. For example, tree_content-1 or 1-2-tree_content will be valid and the value of $tree_id will be respectively.

From this point, our URL will look like this: http://localhost:80?action=tree_content&node=tree_anchor-1. Now it's time to analyze the grow_right_pane_tree() function to exploit the bug.

grow_right_pane_tree_analyze

The vulnerable parameter is rfilter which is directly passed into the RLIKE operand inside WHERE clause. What make it vulnerable is that rfilter is wrap between the double quote ", yet the parameter is checked by the html_validate_tree_vars() function, which looks like this:

function  html_validate_tree_vars() {
/* ================= input validation and session storage ================= */
$filters = array(
	// .......
	'rfilter' => array(
			'filter' => FILTER_VALIDATE_IS_REGEX,
			'pageset' => true,
			'default' => '',
			),
	// ..........
	);

	validate_store_request_vars($filters, 'sess_grt');
	/* ================= input validation ================= */
	// ........
}

The filter type of rfilter is set to FILTER_VALIDATE_IS_REGEX and stored inside $filters, then $filter is passed into validate_store_request_vars() function. Let's dive into this function, this will reveal the rest of our analysis:

function validate_store_request_vars($filters, $sess_prefix = '') {
	// .......
	if (cacti_sizeof($filters)) {
		foreach($filters as $variable => $options) {
		// ....
		elseif ($options['filter'] == FILTER_VALIDATE_IS_REGEX) {
					if (is_base64_encoded($_REQUEST[$variable])) {
						$_REQUEST[$variable] = base64_decode($_REQUEST[$variable]);
					}
					$valid = validate_is_regex($_REQUEST[$variable]);
					if ($valid === true) {
						$value = $_REQUEST[$variable];
					} else {
						$value = false;
						$custom_error = $valid;
					}
				}
// ........
function validate_is_regex($regex) {
// ........
    if (@preg_match("'" . $regex . "'", NULL) !== false) {
		ini_set('track_errors', $track_errors);
		return true;
	}

The validate_store_request_vars() function is responsible for validating user input. When the filter type is FILTER_VALIDATE_IS_REGEX, the function calls the validate_is_regex() function with the preg_match function to verify if our input is valid. According to PHP documentation:

preg_match() returns 1 if the pattern matches given subject, 0 if it does not, or false on failure.

The if statement use Strict comparison meaning that only when error occurs that we cannot step into the if statement. The $regex, which is our input, is wrapped between two single quote '. The intention of the developers is to avoid user inject the single quote ', which is often used to start a SQL injection attack. This seems to be a robust security implementation, yet let's have a look one more time again on how the rfilter is passed into the WHERE clause:

$sql_where  .=  ' (gtg.title_cache RLIKE "'  .  get_request_var('rfilter') .  '" OR gtg.title RLIKE "'  .  get_request_var('rfilter') .  '")';

Ah-oh, The effort of validating user's input become meaningless as the rfilter is wrapped inside the double quotes ", which allows attackers to easily injection " to break out of the statement.

Having understood why it is vulnerable, now we will go deeper to the query where $sql_where is passed into.

Download Tool