前言
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的版本: