Brief analysis of a SQL injection in Cacti 0.8.8b

Back in September 2013 I wanted to practice some code auditing and picked the latest version of Cacti (v0.8.8b at the time). I spent a few hours looking into the code and also assessing a running instance of Cacti and this exercise resulted in a few vulnerabilities. I was motivated to finally put together this write-up since several SQL injections were fixed in Cacti in July 2015. As of this writing (September 2015), it seems like this vulnerability is still present in the latest version of Cacti.

For those who don’t know, Cacti is a quite popular network monitoring tool pretty similar to Zabbix and Nagios. A quick Google search for intitle:”Login to Cacti” comes up with more than 4,000 results. Finding high severity bugs in Cacti means that chances are very high an attacker will actually break into a box located in a privileged position in the network, as it needs to be positioned in a way to monitor traffic and events.

Cacti is a PHP application and I have to say, it’s miserable from a security point of view.

Unfortunately the issue is post-auth (you need to have a valid credential to exploit it) but often one finds Cacti with weak passwords or guest credentials (guest/guest) lurking around.

Vulnerability description

In a tl;dr way

POST /cacti/data_queries.php HTTP/1.1
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 87
Cookie: Cacti=tgts2sshbvapq308fi731flan4


Actual explanation of the vulnerability

The Cacti project was full of SQL injection in the beginning and it still is. Instead of using appropriate countermeasures against SQL injection, they decided to write their own sanitisation functions input_validate_input_*.

However, forgetting to apply the sanitisation function in only one instance can prove to be deadly. They did.

From lib/data_queries.php:

function form_save() {
    if (isset($_POST["save_component_snmp_query"])) {
        $save["id"] = $_POST["id"];
        $save["hash"] = get_hash_data_query($_POST["id"]);  // <----- Calling our buggy function.
        $save["name"] = form_input_validate($_POST["name"], "name", "", false, 3);
        $save["description"] = form_input_validate($_POST["description"], "description", "", true, 3);
        $save["xml_path"] = form_input_validate($_POST["xml_path"], "xml_path", "", false, 3);
        $save["data_input_id"] = $_POST["data_input_id"];

        if (!is_error_message()) {
            $snmp_query_id = sql_save($save, "snmp_query");

            if ($snmp_query_id) {


        header("Location: data_queries.php?action=edit&id=" . (empty($snmp_query_id) ? $_POST["id"] : $snmp_query_id));
    }elseif (isset($_POST["save_component_snmp_query_item"])) {
        /* ================= input validation ================= */
        /* ==================================================== */

        $redirect_back = false;

        $save["id"] = $_POST["id"];

Note that in the second conditional block the POST value of ‘id’ is sanitised, but the process of calling input_validate_* is not applied to ‘id’ in case we have ‘save_component_snmp_query’ set.

The input ends up here, with $data_query_id being user-tainted and with no sanitisation applied to it:

From lib/functions.php:

/* get_hash_data_query - returns the current unique hash for a data query
   @arg $graph_template_id - (int) the ID of the data query to return a hash for
   @arg $sub_type (optional) return the hash for a particlar sub-type of this type
   @returns - a 128-bit, hexadecimal hash */
function get_hash_data_query($data_query_id, $sub_type = "data_query") {
    if ($sub_type == "data_query") {
        $hash = db_fetch_cell("select hash from snmp_query where id=$data_query_id");

In order to better debug the issue on our test system, it was necessary to set MySQL to log slow queries. From mysql-slow.log (logs slow queries) we can see:

# Time: 131025 7:35:59
# User@Host: cactiuser[cactiuser] @ localhost []
# Query_time: 20.371461 Lock_time: 0.000156 Rows_sent: 0 Rows_examined: 0
SET timestamp=1382682959;
select hash from snmp_query where id=0 and benchmark(30000000,sha1(1))–;

Through a time-based attack we could verify the presence of SQL injection and could mount an attack relying on timing differences too.


In Cacti security was not built-in the product but instead brushed upon. Cacti was full of SQL injection vulnerabilities in the past but unfortunately the developers decided to take the easiest (but not the correct) route to fix the core of the issue. Even though the functions used to validate input were enough to mitigate the vulnerability, all it takes is to forget to use it in one single instance. If every database-related function in Cacti passed through a gatekeeper using only parametrized queries instead of dynamically constructed ones, it would be much better off.

Brief analysis of a SQL injection in Cacti 0.8.8b

One thought on “Brief analysis of a SQL injection in Cacti 0.8.8b

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s