一个曾经的 Zabbix SQL 注入漏洞分析
本文原发于知乎专栏,查看原文。
0x00. 漏洞背景
Zabbix 是一个开源的企业级性能监控解决方案。zabbix 的 jsrpc 的 profileIdx2 参数存在 insert 方式的 SQL 注入漏洞,攻击者无需授权登陆即可登陆 zabbix 管理系统,也可通过 script 等功能轻易直接获取 zabbix 服务器的操作系统权限。
通过 zoomeye 搜索 zabbix 组件,共搜索到大约 13963 个主机,而其中很大一部分主机未禁用 guest 账户,因而漏洞影响相对很广。
0x01. 影响版本
1 | version 2.x (<2.2.14) |
0x02. 漏洞说明
1 | POC:url+/jsrpc.php?sid=0bcd4ade648214dc&type=9&method=screen.get×tamp=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 | 参数走向: |
最初的注入产生在 latest.php,这里根据其衍生注入 jsrpc.php 的 poc 进行分析。
jsrpc.php 文件 profileIdx2 参数存在注入。首先跟进 jsrpc 文件 case:'screen.get'(178 行),出现 resourcetype 的 if 判断。

全局搜索 SCREEN_RESOURCE_HISTORY 以及 SCREEN_RESOURCE_CHART,在 include/defines.inc.php 知 SCREEN_RESOURCE_HISTORY = 17,SCREEN_RESOURCE_CHART 为 18;poc 中 resourcetype 参数为 17,满足 if 语句,函数执行至大约 205 行 $screenBase = CScreenBuilder::getScreen($options)。
$options 进入 CScreenBuilder 类的 getScreen 函数,跟进 include/classes/screens/CScreenBuilder.php 函数 148 行。

在 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 行)。

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

接下来我们回到 jsrpc.php,发现末尾包含了 page_footer.php,跟进该文件。在经过一系列 if 语句后,在 46 行处调用了 include/classes/class.cwebuser.php 中 CProfile::flush()。
![CProfile::flush() 函数(54-81 行):userDetails['userid']<=0 直接返回,insert/update 不空则走 insertDB/updateDB](/images/posts/zabbix-jsrpc-sqli-analysis-05.jpg)
跟进 flush 函数:我们看到如果 $insert 或者 $update 变量发生改变,即执行 db 相关操作,调用 insertDB 函数,因此变量最终进入 insertDB 函数 187 行执行,而 include/db.inc.php 中仍然对恶意参数没有任何过滤,进而导致注入产生。

最后我们需要明确两个问题:
1. CWebUser::data 变量为什么始终不为空?
跟进 include/classes/core/Zbase.php:我们可以看到在整个程序框架初始化时会验证用户 session,如果为空,则调用 setDefault() 方法。

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

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

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

0x04. 漏洞证明

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