长亭百川云 - 文章详情

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

bluE0x00

55

2024-07-13

前言

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

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2