CVE-2023-30534: Insecure Deserialization in Cacti prior to 1.2.25
Overview
We discovered two instances of insecure deserialization in Cacti versions prior to 1.2.25, tracked as CVE-2023-30534. Each instance of insecure deserialization can be triggered remotely, but cannot be exploited due to a limited execution environment which is discussed further in the "Attempting Exploitation" section.
For those unfamiliar, Cacti is an open source network management and graphing solution that utilizes RRDtool’s data storage and graphing functionality to discover devices, create graphs, and more. While some instances of Cacti are accessible externally, its primary use is for monitoring internal network systems.
Now, to dive a bit deeper into the technical aspect: Serialization is the process of taking complex data structures like PHP or Java objects and transforming them into a format that is easier to send over the network. Deserialization is when that data gets transformed back into a complex data structure. An insecure deserialization occurs when user-controlled data is deserialized, potentially enabling attackers to instantiate arbitrary objects, read and write files, and even gain remote code execution.
Note: Cacti version 1.2.25 (and 1.3.0) fixes an additional 17 vulnerabilities in Cacti, including critical and high-severity vulnerabilities, so administrators should upgrade when possible.
Affected versions
We originally discovered the insecure deserialization vulnerability in Cacti 1.2.10, and all versions prior to 1.2.25 are vulnerable. Cacti version 1.3.0, released alongside 1.2.5, also fixes the insecure deserialization.
Breaking down the insecure deserialization vulnerabilities
Each instance of insecure deserialization is tied to the usage of the unserialize
function without properly sanitizing user input. While Cacti does offer a “safe deserialization” utility function that attempts to sanitize and verify that the string content only contains specific values before calling unserialize, this function was not used in these instances.
Note: In code blocks, … represents unreferenced code that has been removed for brevity.
managers.php
The vulnerable code is located in the managers.php
file, specifically within the form_actions function. The following code snippet is from Cacti version 1.2.10:
function form_actions() {
global $manager_actions, $manager_notification_actions;
if (isset_request_var('selected_items')) {
if (isset_request_var('action_receivers')) {
...
...
...
} elseif (isset_request_var('action_receiver_notifications')) {
get_filter_request_var('id');
$selected_items = unserialize(stripslashes(get_nfilter_request_var('selected_items')));
...
}
...
}
By setting the POST request variable action_receiver_notifications
, we can direct the code flow to hit the unserialize
call in the form_actions
function. To trigger form_actions
, we need to set the “action” variable to “actions”. This ensures we hit the proper case in the switch statement, as shown below:
switch (get_request_var('action')) {
case 'save':
form_save();
break;
case 'actions':
form_actions();
break;
...
...
To target the vulnerable unserialize
function, an authenticated user sends a POST request that includes a URL encoded serialized PHP object in the “selected_items” variable, as follows:
If we put an invalid object (e.g. one that is malformed or includes a class Cacti does not know about) as the value for the “selected_items” variable, we can see Cacti failing to unserialize the payload in the logs:
At this point, we can control code flow to call unserialize on a user-controlled, serialized object in managers.php.
graphs_new.php
The vulnerable code is located in the graphs_new.php
file, specifically in the host_new_graphs_save
function. The following code snippet is from Cacti version 1.2.10:
function host_new_graphs_save($host_id) {
$selected_graphs_array = unserialize(stripslashes(get_nfilter_request_var('selected_graphs_array')));
$values = array();
This function unserializes the user controlled parameter selected_graphs_array
. While stripslashes
is run first, this simply removes extra slashes that are needed in strings during pre-processing (e.g. \” to “).
In order to reach this function, the user makes a request to graphs_new.php
with two parameters: action=save
and save_component_new_graphs=1
, which causes the vulnerable function to be called, as shown in the code snippet below:
switch (get_request_var('action')) {
case 'save':
form_save();
break;
case 'query_reload':
...
...
...
function form_save() {
...
...
...
if (isset_request_var('save_component_new_graphs')) {
host_new_graphs_save(get_filter_request_var('host_id'));
header('Location: graphs_new.php?host_id=' . get_filter_request_var('host_id') . &header=false');
}
}
To target the vulnerable unserialize
function, an authenticated user sends a POST request that includes a URL encoded serialized PHP object in the selected_graphs_array
variable, as follows:
If we put an invalid object (e.g. one that is malformed or includes a class Cacti does not know about) as the value for the selected_graphs_array
variable, we can see Cacti attempting to unserialize the payload in the logs:
At this point, we can control code flow to call unserialize on a user-controlled, serialized object in graphs_new.php
.
Attempting Exploitation
PHP Magic Methods
Exploiting a PHP deserialization vulnerability requires access to Objects that have “magic methods” implemented. Magic methods are called on objects automatically as they are created, destroyed, and in other situations. Examples of these methods include __construct
, __unserialize
, __destruct
, __wake
, and others. We need access to objects that use these functions because some of them will be automatically called when our payload is unserialized into an Object. In this context, a “gadget” is an object with one of these methods implemented that we can use in a payload. By using nested objects in payloads, we combine these gadgets together into a “gadget chain” to perform a desired action such as reading a file, writing a file, or gaining code execution.
Searching for Gadget Chains
Over time, many gadget chains have been discovered in popular libraries. The PHPGCC tool has a list of known gadget chains that it can use to create payloads for insecure deserialization vulnerabilities. There is one known usable gadget chain in Cacti’s PHP vendors, PHPSecLib. However, Cacti does not require or include any of the necessary gadgets that are needed (TripleDES, AES, and SSH1) in order to successfully use the PHPSecLib gadget chain in a payload.
In addition to making use of existing gadget chains, it’s possible to create new ones if the project has objects with usable magic methods. For example, you can look at cacti’s __construct method implementations by viewing the source code to see if some of those are usable. After looking through the implemented magic methods in Cacti’s PHP objects, none were found to be usable in a new gadget chain.
This leaves the vulnerability not exploitable, because of the inability to find a usable gadget chain.
Nuclei Template for CVE-2023-30534
In order to facilitate quick testing for the vulnerability, as well as to aid remediation efforts, we created a nuclei template that tests for CVE-2023-30534. This template requires authentication and submits an invalid PHP object in order to generate a log message that shows unserialize being called on the object. The template then verifies the unserialize call was reached by checking for the error message in the logs, ensuring the unserialize function was called on our payload. The template being run locally is shown below:
Cacti administrators can remediate the vulnerability by upgrading to version 1.2.5 or later (e.g., 1.3.0). This vulnerability cannot be exploited in a standard Cacti installation, but users should follow the advice provided by Cacti in the security advisory for further details on remediation.
Note: Cacti version 1.2.25 (and 1.3.0) fixes an additional 17 vulnerabilities in Cacti, including critical and high-severity vulnerabilities, so administrators should upgrade as soon as possible.