Cacti命令执行漏洞分析(CVE-2022-46169)
Cacti命令执行漏洞分析(CVE-2022-46169)
Assign: Penguin, 5x
源码分析
1 | https://github.com/Cacti/cacti |
影响版本
Cacti == 1.2.22
漏洞修复
Commit b43f13a
漏洞点定位
lib/functions.php
remote_agent.php
分析
本文的分析将从命令执行的点为起点,进行逆向的分析。
命令执行的点
remote_agent.php的poll_for_data()函数
- 函数开始,首先获取了一些请求变量:
$local_data_ids
、$host_id
和$poller_id
,其中$poller_id
使用的是get_nfilter_request_var()
函数获取参数,即未对输入进行校验。 - 如果
$local_data_ids
数组不为空,则进入foreach
循环遍历$local_data_ids
数组。在循环内部,首先调用了input_validate_input_number()
函数对$local_data_id
进行输入验证。 - 然后执行了一个数据库查询,根据
$host_id
和$local_data_id
从poller_item
表中获取相关记录,并将结果存储在$items
变量中。 - 根据
$items
中从poller_item
获取到的actions值来执行一个switch语句。 case POLLER_ACTION_SCRIPT_PHP
如果action等于常量POLLER_ACTION_SCRIPT_PHP,即action=2,则会执行下面的proc_open
函数,函数的参数将$poller_id
进行了拼接,因此使用管道符|或者``即可进行命令执行
1 | if (cacti_sizeof($local_data_ids)) { |
绕过校验
现在向上分析,看看是谁调用的poll_for_data()函数,可以看到调用函数的位置
就是很简单的判断get传的action参数值,如果为polldata则调用poll_for_data()
函数
1 | switch (get_request_var('action')) { |
可以看到remote_agent.php开头设置了校验,调用了remote_client_authorized()
函数,我们去看看此函数执行了什么操作
1 | function remote_client_authorized() { |
remote_client_authorized()
函数实现的功能为:
- 执行
get_client_addr()
函数,获取客户端地址并将其赋值给$client_addr
变量。 - 然后对
$client_addr
进行一系列的校验 - 调用
remote_agent_strip_domain($client_name)
函数剥离主机名的域名部分,并将结果赋值给$client_name
变量。 - 执行数据库查询,从
poller
表中获取所有记录,并将结果存储在$pollers
数组中。 - 如果
$poller
不为空,则循环遍历$pollers
数组,对比远程客户端的主机名和地址与$poller
记录中的主机名进行比较,如果剥离域名后的$polle
和$client_name
相同则返回true
表示授权。
在数据库中$poller[‘hostname’]默认为localhost,所以只要传入内网地址即可绕过校验。
那么我们如何传入内网地址呢,函数中使用的是get_client_addr()
,我们再看一下这个函数的功能
lib/functions.php
get_client_addr()
循环遍历$http_addr_headers
数组中的每个请求头
在这段代码中,使用break 2
语句可以提前跳出两层循环,导致在匹配到X-Forwarded-For
请求头时就停止了循环,并将$client_addr
设置为该请求头的值。
开发者的本意应该是下面的做法:
1、删除break 2
语句,使循环遍历完所有的请求头,并验证每个请求头中的IP地址。
2、在循环结束后,如果没有找到有效的客户端地址,可以使用$_SERVER['REMOTE_ADDR']
作为默认值
这样做的目的是确保使用最可靠的REMOTE_ADDR
作为客户端地址,避免使用伪造的请求头。
因为开发者错误的写法,攻击者可以发送带有伪造X-Forwarded-For
请求头的请求,从而传入内网地址,绕过remote_client_authorized()
函数的限制。
总结
至此,整个命令执行的流程已经基本逆向地分析完成,再正向的梳理一遍
- 漏洞点位于
remote_agent.php
中,且无需身份验证即可访问此文件。 - 因此我们访问
remote_agent.php
且X-Forwarded-For
请求头伪造为内网地址 - get传action参数值,值为polldata,进入
poll_for_data()
函数 - 真实情况下,local_data_id、host_id需要爆破,以令action=2从而进入执行
proc_open
函数的case proc_open
函数处将$poller_id
参数进行了拼接,因此使用管道符|或者``即可进行命令执行
漏洞复现
1 | GET /remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`id%3E/tmp/1` HTTP/1.1 |