一个曾经的 Zabbix SQL 注入漏洞分析

本文原发于知乎专栏,查看原文

0x00. 漏洞背景

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

通过 zoomeye 搜索 zabbix 组件,共搜索到大约 13963 个主机,而其中很大一部分主机未禁用 guest 账户,因而漏洞影响相对很广。

0x01. 影响版本

1
2
version 2.x (<2.2.14)
version 3.x (<3.0.4)

0x02. 漏洞说明

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

注:该 poc 只在未禁用 guest 账户的情况下生效,禁用 guest 账户时无法进行报错注入。

0x03. 漏洞详情

1
2
3
4
5
6
7
参数走向:
jsrpc.php -> CScreenBuilder.php 构造函数 -> CScreenBase.php calculateTime 函数
-> profiles.inc.php update 函数 -> profiles.inc.php get 函数 -> profiles.inc.php update 函数

sql 语句执行:
page_footer.php 46 行 -> profile.inc.php::flush 函数 -> profiles.inc.php::inserDB 函数
-> db.inc.php::DBexecute 函数

最初的注入产生在 latest.php,这里根据其衍生注入 jsrpc.php 的 poc 进行分析。

jsrpc.php 文件 profileIdx2 参数存在注入。首先跟进 jsrpc 文件 case:'screen.get'(178 行),出现 resourcetype 的 if 判断。

jsrpc.php 第 178 行起 `case 'screen.get'` 段,可见 resourcetype 判断与 CScreenBuilder::getScreen 调用

全局搜索 SCREEN_RESOURCE_HISTORY 以及 SCREEN_RESOURCE_CHART,在 include/defines.inc.phpSCREEN_RESOURCE_HISTORY = 17SCREEN_RESOURCE_CHART 为 18;poc 中 resourcetype 参数为 17,满足 if 语句,函数执行至大约 205 行 $screenBase = CScreenBuilder::getScreen($options)

$options 进入 CScreenBuilder 类的 getScreen 函数,跟进 include/classes/screens/CScreenBuilder.php 函数 148 行。

CScreenBuilder 构造函数第 148 行附近的 calculateTime 参数数组

在 CScreenBuilder 类的构造函数中(大约 148 行),我们可以看到 profileIdx2 参数进入了 CScreenBase 类 calculateTime 函数。

继续跟进 include/classes/screens/CScreenBase.php,我们看到经过 344 行的 if 语句判断,profileIdx2 进入 CProfile 类的 update 函数。

跟进 include/classes/profiles.inc.php 中 CProfile 类的 update 方法(大约 154 行)。$current 由 get 函数获得,跟进 get 函数(88 行)。

CProfile::update(138-176 行),含 154 行 $current = CProfile::get 与 161 行 self::$insert 赋值

我们打印 CWebUser::data 发现,在不禁用以及禁用 guest 账户两种情况下,该变量均不为空;而根据 profile 变量可知 get 函数中两个 if 语句均不成立,函数在 104 行返回空值。进而 update 方法中 $current 变量为空,函数执行至 161 行 self::$insert[$idx][$idx2] = $profile; —— 到这儿程序仍未执行任何 db 操作,仅仅把参数代入了 $insert 变量。

CProfile::get 函数 88-106 行,可见 var_dump(CWebUser::$data) 与 104 行返回空值

接下来我们回到 jsrpc.php,发现末尾包含了 page_footer.php,跟进该文件。在经过一系列 if 语句后,在 46 行处调用了 include/classes/class.cwebuser.phpCProfile::flush()

CProfile::flush() 函数(54-81 行):userDetails['userid']<=0 直接返回,insert/update 不空则走 insertDB/updateDB

跟进 flush 函数:我们看到如果 $insert 或者 $update 变量发生改变,即执行 db 相关操作,调用 insertDB 函数,因此变量最终进入 insertDB 函数 187 行执行,而 include/db.inc.php 中仍然对恶意参数没有任何过滤,进而导致注入产生。

最终执行 SQL 的位置:db.inc.php 第 189 行 `DBexecute('INSERT INTO profiles (...) VALUES (...)')`,profileIdx2 直接拼接到 SQL 语句里

最后我们需要明确两个问题:

1. CWebUser::data 变量为什么始终不为空?

跟进 include/classes/core/Zbase.php:我们可以看到在整个程序框架初始化时会验证用户 session,如果为空,则调用 setDefault() 方法。

Zbase.php 中 authenticateUser(374-384 行):session 校验失败时调用 CWebUser::setDefault()

跟进 include/classes/class.cwebuser.phpsetDefault() 方法:我们发现 setDefault() 方法会默认赋值给 data。因此未禁用以及禁用 guest 账户两种情况,CWebUser::data 都不会为空。

setDefault()(111-120 行):data 数组默认值——userid=0、alias=ZBX_GUEST_USER

2. 禁用 guest 账户为什么无法进行注入?

我们看到执行 flush 函数是需要 userid > 0 的。当未禁用账户时,虽然 setDefault() 方法会使得 userid = 0,我们跟进 include/classes/class.cwebuser.php 中 checkAuthentication 函数。

checkAuthentication(79-92 行):session 为空时调用 setDefault(),并以 ZBX_GUEST_USER 调 API::User()->login

defines.inc.php 知,ZBX_GUEST_USER 为 guest 账户,guest 账户 userid = 2,因此在未禁用 guest 账户时可以进行注入。

我们继续跟进 API::User()->login() 方法:我们发现这儿对 userid 有权限检查,当我们禁用 guest 账户时,就会在 1066 行产生异常,以至于 userid 仍然为 0,flush 函数无法执行,因此禁用 guest 账户时无法进行注入。

API::User()->login() 第 1065-1067 行:check_perm2system 失败时抛 ZBX_API_ERROR_PARAMETERS,导致 userid 仍为 0、flush 不执行

0x04. 漏洞证明

注入报错截图:访问 127.0.0.1/zabbix2.2.13rc/jsrpc.php?method=screen.get&profileIdx2=... 触发的 XPATH syntax error,profileIdx2 被直接拼进 INSERT 语句执行

0x05. 漏洞修复

  1. include/classes/class.cwebuser.php 中 flush 函数,将 75 行 foreach 处 $idx2 更改为 zbx_dbstr($idx2),即使用 zabbix 自带过滤函数对参数 $idx2(即 profileIdx2 参数)进行过滤(临时方案)
  2. Version 2.x 版本更新至 2.2.14,Version 3.x 版本更新至 3.0.4(推荐)
评论Comments