Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
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
32 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.

root@kitploit:~
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:

root@kitploit:~
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.

root@kitploit:~
<?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:

root@kitploit:~
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:

root@kitploit:~
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:

root@kitploit:~
$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.

grow_right_pane_tree_analyze

After $sql_where is assigned, the program will call the get_allowed_tree_header_graphs() function. The function will look like this:

root@kitploit:~
function  get_allowed_tree_header_graphs($tree_id, $leaf_id = 0, $sql_where = '', $sql_order = 'gti.position', $sql_limit = '', &$total_rows = 0, $user_id = 0) {
	// ......
	if ($sql_where != '') {
		$sql_where = " AND ($sql_where)";
}
	$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;
	
	$graphs = db_fetch_assoc("SELECT gti.id, gti.title, gtg.local_graph_id, h.description, gt.name AS template_name, gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id
	FROM graph_templates_graph AS gtg
	INNER JOIN graph_local AS gl
	ON gl.id = gtg.local_graph_id
	INNER JOIN graph_tree_items AS gti
	ON gti.local_graph_id = gl.id
	LEFT JOIN graph_templates AS gt
	ON gt.id = gl.graph_template_id
	LEFT JOIN host AS h
	ON h.id = gl.host_id
	$sql_where
	$sql_order
	$sql_limit");

	$sql = "SELECT  COUNT(*)
	FROM graph_templates_graph AS gtg
	INNER JOIN graph_local AS gl
	ON gl.id=gtg.local_graph_id
	INNER JOIN graph_tree_items AS gti
	ON gti.local_graph_id=gl.id
	LEFT JOIN graph_templates AS gt
	ON gt.id=gl.graph_template_id
	LEFT JOIN host AS h
	ON h.id=gl.host_id
	$sql_where";
	$total_rows = get_total_row_data($user_id, $sql, array(), 'graph');
	return  $graphs;
}

This is where the $sql_where is passed into a query. Note that the variable is assigned 2 more times before being passed in, which are $sql_where = " AND ($sql_where)"; and $sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;. Combine with the first assignment, this is how the $sql_where is finally created:

root@kitploit:~
$sql_where  .=  ' (gtg.title_cache RLIKE "'  .  get_request_var('rfilter') .  '" OR gtg.title RLIKE "'  .  get_request_var('rfilter') .  '")';
$sql_where = " AND ($sql_where)";
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;

After these 3 assignments, the $sql_where will look like this:

root@kitploit:~
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id) AND ((gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '"))";

The query that $sql_where is passed into looks overwhelming, yet we can create another simple query with similar structure:

select * from users WHERE (TRUE) AND ((username RLIKE "' get_request_var('rfilter') '" -- something behind that isn't neccessary))

In order to successfully inject our payload, we need to break out of the " and )). Therefore, this payload may work "));SELECT SLEEP(5) --. Our payload will look like this, select * from users WHERE (TRUE) AND **((username RLIKE "'"));SELECT SLEEP(5) --. Looks good! Let's try on Cacti

failed_injection_1

The reason the server doesn't sleep for 2 seconds is because we caused error in the preg_match() function. Let's look at the function again

root@kitploit:~
if (@preg_match("'"  .  $regex  .  "'", NULL) !== false) {
ini_set('track_errors', $track_errors);
return  true;
}

This function will take our input as regular expression. And the ) is used to group a set of characters. Because it's not closed, so the function returned error. On the other hand, if we try "(());SELECT SLEEP(5) --, we cannot break out of the (( from the SQL query. However, there a trick here, and it's is "OR"(("));SELECT SLEEP(5) --. Let me explain this, I wrap (( in double quote to make it as a string in SQL query and use OR to match with the username before it. The, double quote " in regular expression, however, is just considered as a normal character, that's why we can take advantage of this feature to break out of the SQL query.

successful_injection

Based on the response time, we can make sure that we successfully injected SQL into the database. This is a critical SQL injection vulnerability. The attacker can leverage this bug to take over admin account or even execute command remotely.

Mitigation

As usual, injection vulnerabilities in general occur due to the absence of input validation. However, in this case, the developers applied validations, yet improperly. In order to fix this, we just simply wrap the rfilter parameter inside the single quote ' instead of double quote "

fix

With this simple fix, the effort of validating user's input will now work properly. Let's try the payload again to see if it's still vulnerable.

fixed_request

With the same payload but now the response is really fast, meaning that we cannot inject SQL command anymore.


From this CVE analysis, we can learn that even minor mistake can also result in a catastrophic outcome. We should carefully review the code and frequently test our website to make it more and more secure. This is the end of the analysis, hope you learnt something useful today. Happy hacking!

Download Tool