
Cacti 1.2.22 unauthenticated command injection
| Field | Details |
|---|
| Product | Cacti |
| Affected version | 1.2.22 |
| Vulnerability | Unauthenticated OS command injection |
| CVE | CVE-2022-46169 |
| CWE | CWE-77 — Command Injection |
| Severity | Critical |
| CVSS v3.1 | 9.8 |
| Attack vector | Network |
| Authentication | None |
| User interaction | None |
| Impact | Confidentiality / Integrity / Availability |
| Fixed version | 1.2.23 |
| Vulnerable component | remote_agent.php |
| Additional component | lib/functions.php |
| Main vulnerable action | polldata |
The published CVSS vector is:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
The official advisory rates it 9.8 Critical. (GitHub)
There are actually two bugs chained together.
Stage 1 — Authorization bypass
Cacti's remote_agent.php accepts requests without normal authentication, but attempts to determine whether the requester is an authorized poller.
The authorization flow effectively does:
HTTP request
│
▼
remote_agent.php
│
▼
remote_client_authorized()
│
▼
get_client_addr()
│
▼
gethostbyaddr()
│
▼
poller table hostname comparison
The problem is get_client_addr().
In 1.2.22 it examines numerous HTTP-derived server variables, including forwarded-client-IP headers. The advisory explains that attacker-controlled HTTP_* values can influence the IP returned by this function. (GitHub)
That means the application can be tricked into believing:
Attacker
↓
"my IP is the Cacti server"
↓
gethostbyaddr()
↓
Cacti server hostname
↓
matches poller table
↓
AUTHORIZED
So the attacker doesn't need a legitimate Cacti account.
After bypassing the authorization check, the interesting endpoint functionality is the polldata action.
The relevant execution path is approximately:
remote_agent.php
│
▼
polldata
│
▼
poll_for_data()
│
├── host_id
├── local_data_ids
└── poller_id
│
▼
poller_item lookup
│
▼
POLLER_ACTION_SCRIPT_PHP
│
▼
proc_open()
│
▼
OS command execution
The important mistake is the handling of poller_id.
The application retrieves it using:
get_nfilter_request_var()
rather than enforcing that it is an integer.
That attacker-controlled value eventually becomes part of a command passed to PHP's proc_open(). The official advisory explicitly identifies this as the command-injection primitive. (GitHub)
Conceptually:
attacker-controlled input
↓
poller_id
↓
string concatenation
↓
proc_open()
↓
operating-system command
This is the critical part of the vulnerability.
The interesting thing for your PoC analysis is that neither bug alone is the whole story.
It's a vulnerability chain:
┌──────────────────────────┐
│ Unauthenticated attacker │
└────────────┬─────────────┘
│
▼
remote_agent.php
│
▼
Authorization bypass
via client IP logic
│
▼
polldata
│
▼
poller_item lookup
│
▼
POLLER_ACTION_SCRIPT_PHP
│
▼
attacker input
→ poller_id
│
▼
proc_open()
│
▼
Command execution
│
▼
RCE
That's a very important distinction to make in your write-up:
CVE-2022-46169 is not simply "a bad parameter in remote_agent.php." It is a chained authorization-bypass + command-injection vulnerability.
The official advisory confirms that the vulnerable execution condition requires a poller_item whose action is POLLER_ACTION_SCRIPT_PHP. (GitHub)
Your PoC should explicitly document this because it is an important analytical detail.
The target needs a suitable poller_item configured with:
POLLER_ACTION_SCRIPT_PHP
The Cacti advisory notes that this is common on production installations because predefined templates such as Device - Uptime and Device - Polling Time can create these entries. (GitHub)
So don't write:
"Every Cacti 1.2.22 installation is automatically exploitable."
A more technically accurate statement is:
Cacti 1.2.22 is vulnerable, and successful command execution depends on the presence of an appropriate
poller_itemconfiguration.
host_id and local_data_id matterpoll_for_data() doesn't simply execute the supplied poller_id.
It first queries poller_item using values corresponding to:
host_id
local_data_id
Then it examines the resulting item's action.
The vulnerable condition is effectively:
host_id
+
local_data_id
↓
poller_item
↓
action == POLLER_ACTION_SCRIPT_PHP
↓
vulnerable execution path
The original advisory notes that these identifiers can be discovered because the relevant entries exist in the application's database, and that suitable entries are likely to exist on productive installations. (GitHub)
For a public PoC, I'd demonstrate this prerequisite explicitly rather than hiding it.
For something you're publishing, I recommend making the PoC demonstrate command execution without giving readers a weaponized reverse-shell payload.
For example, structure your demonstration as:
1. Deploy Cacti 1.2.22 in an isolated VM
2. Configure a poller_item using POLLER_ACTION_SCRIPT_PHP
3. Confirm remote_agent.php is reachable
4. Demonstrate the authorization decision being influenced
5. Reach the polldata execution path
6. Use a harmless command-execution marker
7. Capture the resulting application/log evidence
8. Upgrade to 1.2.23
9. Repeat the test
10. Demonstrate that the vulnerability is no longer exploitable
That gives you a legitimate vulnerability demonstration without turning the write-up into a ready-made Internet RCE weapon.
You can divide the vulnerable code into three areas.
remote_agent.phpResponsible for exposing the remote-agent functionality and dispatching the requested action.
remote_agent.php
│
└── action = polldata
│
▼
poll_for_data()
lib/functions.phpContains get_client_addr().
The problematic design is trusting HTTP-derived values when deciding the requester's actual network address.
The official advisory lists multiple HTTP-related variables that are inspected before falling back to the actual remote address. (GitHub)
proc_open()The final dangerous sink is the construction of the PHP command executed through proc_open().
The advisory identifies the vulnerable flow as:
poller_id
↓
get_nfilter_request_var()
↓
command construction
↓
proc_open()
(GitHub)
The Cacti advisory identifies two important remediation changes.
First, poller_id should be treated as an integer:
get_nfilter_request_var()
↓
get_filter_request_var()
Second, the value should additionally be shell-escaped before being incorporated into the command:
escapeshellarg($poller_id)
The advisory specifically recommends both measures. (GitHub)
The authorization side also needs to stop allowing an attacker to arbitrarily influence the client IP used for authorization. (GitHub)
Cacti 1.2.22
│
├── vulnerable
├── authorization bypass
├── command injection
└── potential unauthenticated RCE
Cacti 1.2.23
│
└── CVE-2022-46169 patched
The vendor advisory lists 1.2.22 as affected and 1.2.23 as the patched 1.2.x release. (GitHub)
Amazon's security advisory independently describes the issue as allowing an unauthenticated attacker to execute arbitrary commands and gives it a 9.8 CVSS score. (AWS Training and Certification)
I'd use something like:
CVE-2022-46169 — Unauthenticated Command Injection / RCE in Cacti 1.2.22
Or more technical:
CVE-2022-46169: Analysis of the Cacti 1.2.22 Remote Agent Authorization Bypass and Command Injection Chain
And your vulnerability summary can say:
Cacti 1.2.22 contains a critical vulnerability chain in the remote-agent functionality. An attacker can manipulate the client-address determination logic to bypass the remote-agent authorization check. Once the
polldatafunctionality is reached, insufficient validation of thepoller_idparameter allows attacker-controlled data to reach a command executed throughproc_open(). Under configurations containing aPOLLER_ACTION_SCRIPT_PHPpoller item, this can result in unauthenticated remote command execution. The issue was fixed in Cacti 1.2.23. (GitHub)
One important correction for your research: don't confuse this with CVE-2023-39362. That later Cacti RCE affects 1.2.24 and was fixed in 1.2.25, so it isn't the vulnerability you want for a 1.2.22 PoC. (NVD)