Cacti is an open-source, web-based monitoring solution with a long-standing history dating back to its first release in 2001. Nowadays, it is well established, actively maintained, and deployed worldwide. A quick Shodan search reveals that thousands of organizations publicly expose their instances to the internet.
To continuously improve the technology behind our Clean Code solution, we regularly scan open-source projects and evaluate the results. In the case of Cacti, our engine reported a promising command injection vulnerability. Analyzing this finding revealed that an unauthenticated attacker can exploit the vulnerability by leveraging an authentication bypass.
This article will outline the impact and deep dive into the technical details of the discovered vulnerabilities. Furthermore, we will determine the root cause of the vulnerabilities and explain how the applied patches mitigate them.
Impact
The vulnerabilities affect Cacti version 1.2.22 and below and are tracked as CVE-2022-46169 with a CVSS score of 9.8. Unauthenticated attackers could exploit a vulnerable Cacti instance if any monitored device uses a specific data source. Exploiting allows attackers to run arbitrary commands under the same user as the web server process is running.
The following video demonstrates the exploitation of a server running a vulnerable version of Cacti:
The security advisory contains a patch that system administrators must apply manually for Cacti versions 1.2.22 and below. The patch will be released as part of versions 1.2.23 and 1.3.0.
We strongly recommend applying the provided patches and updating to a new version once available.
Technical Details
In this section, we look at the vulnerability reported by SonarCloud and determine how an attacker can exploit it. The attack we demonstrate is made of two distinct code vulnerabilities:
- Authentication Bypass: a hostname-based authorization check is not implemented safely for most installations of Cacti
- Command Injection: unsanitized user input is propagated to a string used to execute an external command
Authentication Bypass
The script remote_agent.php
is supposed to be accessed by authorized clients only. For this reason, there is an authorization check at the beginning of the file:
cacti/remote_agent.php
The function remote_client_authorized
retrieves the IP address of the client ($client_addr
), resolves it to the corresponding hostname ($client_name
) and checks if the poller
table contains an entry with this hostname:
cacti/lib/html_utility.php
The above code snippet shows that the function get_client_addr
retrieves the IP address of the client. This function takes into account a variety of attacker-controllable HTTP headers when determining the IP address:
cacti/lib/functions.php
While the REMOTE_ADDR
variable is set to the source IP address from the connection to the web server, variables beginning with HTTP_
are populated by the corresponding HTTP headers received from the client. Attackers can fully control these values if there is no instance between the client and the web server (i.e., a reverse proxy) that would filter these HTTP headers.
Coming back to the former code snippet, the poller
table contains a default entry with the hostname of the server running Cacti. Because of this, attackers can bypass the remote_client_authorized
check by, e.g., providing the HTTP header X-Forwarded: <TARGET-IP>
. This way, the function get_client_addr
returns the IP address of the server running Cacti. The call to gethostbyaddr
resolves this IP address to the hostname of the server, which will pass the poller hostname check because of the default entry.
This allows unauthenticated attackers to access the functionality of remote_agent.php
.
Command Injection Vulnerability
Scanning Cacti with SonarCloud revealed an interesting command injection vulnerability in remote_agent.php
. You can inspect the finding directly on SonarCloud:
Try it by yourself on SonarCloud!
According to the outlined injection flow, the user-provided parameter poller_id
is propagated to the first parameter of proc_open
without any sanitization or escaping. This introduces a command injection vulnerability in the poll_for_data
function.
Attackers can trigger the vulnerable function by setting the action
parameter to polldata
:
cacti/remote_agent.php
In the beginning, the poll_for_data
function retrieves the parameters host_id
and poller_id
. However, there is an essential difference: The host_id
parameter comes from get_filter_request_var
, while the poller_id
parameter comes from get_nfilter_request_var
; notice the additional n
character here:
cacti/remote_agent.php
While the get_filter_request_var
function verifies that the retrieved parameter is an integer, get_nfilter_request_var
, which is used to retrieve the poller_id
parameter, allows arbitrary strings.
Further following the injection flow, we can see that poller items are retrieved from the database. If the action of one of these items is set to POLLER_ACTION_SCRIPT_PHP
, the vulnerable call to proc_open
is issued:
cacti/remote_agent.php
This means that attackers can leverage the poller_id
parameter to inject an arbitrary command when an item with the POLLER_ACTION_SCRIPT_PHP
action exists. This is very likely on a productive instance because this action is added by some predefined templates like "Device - Uptime"
or "Device - Polling Time"
.
The attacker must provide the corresponding id to make the database query return such an item. Since the ids are numbered in ascending order and hundreds of ids can be sent in a single request by providing an array, attackers can easily discover a valid identifier.
Patches
Authentication Bypass
The authentication bypass was mitigated by allowing the administrator to configure which HTTP proxy headers should be honored when determining the IP address of a client. Only the REMOTE_ADDR
server variable is used by default, ensuring a secure default configuration.
Additionally, this patch allows administrators to use HTTP proxy headers, e.g., in scenarios where the Cacti instance is behind a reverse proxy.
Command Injection
The command injection vulnerability was mitigated with two fixes applied to the source (retrieval of user input) and the sink (call to proc_open
). At the source, the function get_nfilter_request_var
was replaced with get_filter_request_var
to ensure that the poller_id
parameter is an integer:
cacti/remote_agent.php
At the sink, the $poller_id
variable was escaped via cacti_escapeshellarg
before being inserted into the command string of proc_open
:
cacti/remote_agent.php
This second fix may seem unnecessary, as the validation at the source already ensures that the variable contains an integer. However, adjusting the source code may change this assumption in the future, reintroducing a critical vulnerability.
Because of this, both fixes are essential: user input should always be validated and restricted to the assumed values (an integer in this case). Furthermore, values should always be escaped before being passed to sensitive functions like proc_open
.
Timeline
Date | Action |
2022-12-02 | We report all issues to vendor |
2022-12-02 | Vendor confirmes the issues |
2022-12-02 | Vendor provides patch via security advisory |
Based on our information, the same vulnerabilities were independently discovered by @stevenseeley and reported via ZDI on 2022-11-25. Further details are not available at the time of writing.
Summary
In this article, we detailed a critical command injection vulnerability in the IT monitoring solution Cacti. This code vulnerability is automatically detected by our scanning engine. We also uncovered a bug in the authentication mechanism, allowing its exploitation from an unauthenticated position. We also looked at the patches applied to fix the vulnerabilities.
The patches and the fact that either of the two applied fixes for the command injection vulnerability would have prevented it highlights how important it is to apply security on all layers. Because of this, an essential part of our Clean Code approach is to embed security as an integral part of development. This ensures that security considerations are not only applied to the current state of the source code, reducing the risk of introducing new vulnerabilities.
Finally, we would like to thank the Cacti maintainers (@netniV, @TheWitness), who almost instantly verified the issues and provided a comprehensive patch!