Cacti-远程命令执行分析[CVE-2022-46169]

· 2022-12-16 00:00 · 0 阅读

原创 深蓝 2022-12-16 00:00 北京

前言

Cacti项目是一个开源平台,可为用户提供强大且可扩展的操作监控和故障管理框架。

由于remote_agent.php中的case POLLER_ACTION_SCRIPT_PHP 在使用proc_open函数时未对传入的poller_id参数做严格过滤,攻击者可构造满足条件的payload对相关目标系统进行命令注入,导致远程命令执行。

影响范围:

Cacti == 1.2.22


正文

由于没有公开的poc,参考官方修复方案与漏洞描述进行poc挖掘与复现。

环境搭建参考:

https://blog.csdn.net/katrina0602/article/details/103710840

主要看remote_agent.php:

在进行下一步操作前会调用remote_client_authorized()进行身份验证:

首先会通过get_client_addr()函数获取client_addr,然后从数据库中获取pollers对象集合逐一比对poller的hostname字段,而get_client_addr()会从下述header头中取出client_addr的值。

要满足上述条件,显然我们需要数据库中有一个poller满足hostname与我们通过上述header头传输的client_addr值相等:

同时经过测试X-Forwarded-For,Forwarded-For ,Client-ip以HTTP开头字段(经p师傅指点)能满足上述传值的条件:

至此我们成功绕过了身份校验访问到了remote_agent.php:

接下来,会进入一个action的case逻辑中,可以看到polldata的逻辑中会调用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;
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));
if (cacti_sizeof($items)) { foreach($items as $item) { switch ($item['action']) { case POLLER_ACTION_SNMP: /* snmp */ if (($item['snmp_version'] == 0) || (($item['snmp_community'] == '') && ($item['snmp_version'] != 3))) { $output = 'U'; } else { $host = db_fetch_row_prepared('SELECT ping_retries, max_oids FROM host WHERE hostname = ?', array($item['hostname'])); $session = cacti_snmp_session($item['hostname'], $item['snmp_community'], $item['snmp_version'], $item['snmp_username'], $item['snmp_password'], $item['snmp_auth_protocol'], $item['snmp_priv_passphrase'], $item['snmp_priv_protocol'], $item['snmp_context'], $item['snmp_engine_id'], $item['snmp_port'], $item['snmp_timeout'], $host['ping_retries'], $host['max_oids']);
if ($session === false) { $output = 'U'; } else { $output = cacti_snmp_session_get($session, $item['arg1']); $session->close(); }
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; case POLLER_ACTION_SCRIPT: /* script (popen) */ $output = trim(exec_poll($item['arg1']));
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; 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]);
$return_value = proc_close($cactiphp); }
break; }
$i++; } } } }
print json_encode($return);}

首先会从请求中获取到$local_data_ids,$host_id,$poller_id三个参数:

get_filter_request_var会对传入的值进行过滤:

get_nfilter_request_var可以获取多组数据但未进行过滤。

function get_nfilter_request_var($name, $default = '') {   global $_CACTI_REQUEST;
if (isset($_CACTI_REQUEST[$name])) { return $_CACTI_REQUEST[$name]; } elseif (isset($_REQUEST[$name])) { return $_REQUEST[$name]; } else { return $default; }}

获取完相关参数后,接下来会从poller_item这张表中取出所有符合满足条件的items进行遍历:

在查询出的item满足POLLER_ACTION_SCRIPT_PHP也就是2时,将会把获取到的poller_id拼接到proc_open命令中,配合前面提到的poller_id的传输未进行任何相关过滤操作,造成命令注入,并最终执行任意命令:

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; }


后记

官方补丁修复:

对get_client_addr()方法进行了重写

并用包含过滤逻辑的get_filter_request_var()函数来获取poller_id参数:

针对PHP <5.4的版本:



阅读原文

跳转微信打开