TeamCity CVE-2024-27198 认证绕过漏洞 — 0day 视角的发现与复现方法

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。

系列说明:本文是该系列的第一篇,聚焦漏洞的成因分析、实验环境和 0day 视角的发现方法。关于漏洞的利用影响、在野态势、披露争议以及语义漂移模式的横向分析,见第二篇:[[teamcity-cve-2024-27198-impact]]。

漏洞编号CVE-2024-27198
CWE分类CWE-288(备用路径导致认证绕过)
发现时间2024年2月(由 Rapid7 的 Stephen Fewer 发现)
CVSS评分9.8 (Critical / 严重)
受影响版本JetBrains TeamCity On-Premises <= 2023.11.3
修复版本JetBrains TeamCity 2023.11.4
危害描述未经身份验证的攻击者可通过特定的URL路径绕过认证机制,获取管理员权限并接管TeamCity服务器,进而可能导致供应链投毒攻击。

目录

  1. 引言与漏洞背景
  2. 漏洞成因分析
  3. 漏洞触发环境与复现条件
  4. 漏洞发现方法与过程(0day视角)
  5. 防御机制与突破点分析
  6. 修复与缓解建议
  7. 结论:可迁移的发现方法论
  8. 参考与来源

1. 引言与漏洞背景

TeamCity 是 JetBrains 开发的一款广泛使用的 CI/CD(持续集成与持续交付)服务器,在企业软件开发生命周期中占据核心地位。由于其掌控着代码构建与部署权限,一旦被攻陷,极易引发大范围的软件供应链攻击。

2024年2月,Rapid7 漏洞研究团队在 TeamCity 的 Web 组件中发现了两个严重的认证绕过漏洞(CVE-2024-27198 与 CVE-2024-27199)。其中,CVE-2024-27198(CVSS 9.8)允许未经身份验证的攻击者直接获取最高管理权限并实现远程代码执行(RCE)。

我关注这个漏洞,不只是因为它严重,更因为它的根因很有代表性——不是某个鉴权函数"写错了",而是请求在多个处理层之间被不同组件"重新理解",导致鉴权边界和执行边界发生了错位。这种问题单看代码很难发现,常规扫描器也抓不住,但它有一套可以复用的发现方法论。

本文的核心目标就是还原这套方法论:在不依赖公开利用样例、不先验知道答案的前提下,从零开始走一遍"发现"的过程。

2. 漏洞成因分析

本节内容依据 Rapid7 官方报告 analysis 部分进行翻译与整理,重点还原"代码层为什么会绕过",而非仅描述复现步骤。

该漏洞本质属于 CWE-288(Authentication Bypass Using an Alternate Path):请求在不同处理层被解释为不同语义,导致鉴权边界与执行边界错位。

认证语义与执行语义错位示意图 图2-1 鉴权层与执行层对同一请求语义解释不一致,是本漏洞的核心机制。

Rapid7 在报告中指出,关键问题位于 web-openapi.jar 内的 jetbrains.buildServer.controllers.BaseController。在 handleRequestInternal() 中,当请求未触发重定向时,会进入 updateViewIfRequestHasJspParameter() 逻辑;该逻辑会读取请求中的 jsp 参数并尝试覆盖 ModelAndView 的视图目标。

结合该控制流可得到漏洞成立的三个条件(Rapid7 原文同样给出了示例语义):

  1. 入口必须是未认证可达且可进入后续处理链的路径 例如一个不存在路径触发 404 处理流。这里的目的不是访问该路径本身,而是让请求进入 BaseController 的后续逻辑。

  2. 请求中携带可影响目标视图/路由的参数 jsp 参数值被设置为受保护的 REST 路径后,会影响 ModelAndView 的最终视图解析目标。

  3. 请求路径语义被伪装为 JSP 相关形式 在路径中引入分号参数与 .jsp 后缀语义后,不同层(容器、鉴权、控制器)对 URI 的解释出现分歧:上层可能把它当成可放行的 JSP 视图请求,而后续路由阶段可能将其规范化/截断后命中真实受保护 API。

因此,这不是"单点逻辑缺陷",而是一个多层解析不一致问题: 鉴权层看到的是"看似无害的视图请求",执行层命中的是"受保护 REST 端点"。一旦错位成立,未认证请求即可跨越访问控制边界,进一步触发管理员级操作链路,最终导致服务器完全失陷风险。

3. 漏洞触发环境与复现条件

3.1 复现实验环境

  • 操作系统:Windows 11(宿主机)
  • 容器环境:Docker Desktop
  • 目标版本jetbrains/teamcity-server:2023.11.3(漏洞版)
  • 对照版本jetbrains/teamcity-server:2023.11.4(修复版)

踩坑备注:[待补充:Docker 部署过程中遇到的配置问题、初始化步骤、需要注意的版本差异等]

3.2 触发前提条件

漏洞触发并非"单一 URL 命中",而是需要满足一组语义条件:

  1. 请求首先进入一个未认证可达的处理流(常见是错误路径处理流)。
  2. 请求参数中存在可影响视图/路由目标的输入(如 jsp 参数语义)。
  3. 请求路径外观被构造成 JSP 相关语义(例如路径后缀和分号参数语义),使不同处理层产生解析分歧。

这三个条件叠加后,可能出现"上层鉴权判断通过,但下层路由命中受保护 API"的错位。

3.3 复现实验判定标准

本报告采用"对照验证"而非单点命中验证:

  • 同一类输入在漏洞版(2023.11.3)出现未授权异常可达行为;
  • 同一输入在修复版(2023.11.4)被拒绝或回到正常鉴权行为;
  • 结合代码层分析,能够解释该差异与路径语义解析不一致有关。

4. 漏洞发现方法与过程

本节仅按 0day 视角还原"当时如何发现": 从未授权基线出发,经通用语义变异发现异常,再通过旧版反编译做灰盒收敛,最后用第二轮定向样本完成复证闭环。

0day视角发现流程图 图4-1 0day视角发现路径:基线建模 -> 语义变异 -> 灰盒收敛 -> 定向验证。

4.1 方法论与研究约束

为了避免"先知道答案再倒推",本次实验设定了四个约束:

  1. 第一轮黑盒不直接使用公开利用样例,也不先假设参数名。
  2. 先建立未授权访问基线,再做通用语义变异。
  3. 只有当异常稳定出现后,才进入反编译灰盒收敛。
  4. 后续公开资料仅用于结果对照,不作为前置发现线索。

对应的分析链路是: 基线建模 -> 第一轮通用变异 -> 灰盒收敛 -> 第二轮定向验证

4.2 阶段A:未授权基线建模(0day 起点)

先回答一个最基础的问题:目标在未登录状态下"正常应该返回什么"? 我们对根路径、登录页面、REST 接口和错误路径做了基线测量,得到三类稳定外观:

  • 受保护入口(如 /overview.html/app/rest/server):以 401 text/plain 为主。
  • 登录相关页面(如 /login.html):200 text/html
  • 错误路径(如 /does-not-exist):404 text/html

这一步的价值是建立"异常参照系"。后续若错误路径不再 404,或未授权请求出现受保护 API 语义,就属于高优先级异常。

4.3 阶段B:第一轮通用语义变异(不押答案)

第一轮只测试通用 Web 路径语义,不带特定可疑参数。典型样本包括:

/does-not-exist
/does-not-exist/
/does-not-exist//
/does-not-exist;x
/does-not-exist.jsp
/does-not-exist/.jsp

关键观察是:

  • /does-not-exist 返回 404 text/html
  • /does-not-exist.jsp/does-not-exist/.jsp 返回 401 text/plain

这说明单纯 .jsp 外观就能改变处理流:请求从"错误页处理链"偏向"认证相关处理链"。 此时还不知道 jsp 参数,但已经能确定灰盒分析应优先关注 .jsp、视图解析、控制器参数读取。

4.4 阶段C:旧版反编译灰盒收敛

本阶段并非先验锁定 web-openapi.jar,而是先在 WEB-INF/libweb-* 模块(web-core.jarweb-openapi.jarweb-startup.jarweb-diagnostic.jar)中做并行筛查。筛查关键词来自阶段B暴露出的行为特征,即 .jspModelAndViewsetViewNamegetParameter

检索结果显示,相关控制器线索主要集中在 jetbrains/buildServer/controllers/BaseController.java,且命中位于 web-openapi 反编译结果中;同样检索在 web-core 中未出现对应控制器命中。由此,分析重点自然收敛到 web-openapi.jar

2023.11.3 旧版 web-openapi.jar 中继续反编译和阅读,可定位到 BaseController 的核心逻辑:

private void updateViewIfRequestHasJspParameter(HttpServletRequest request, ModelAndView modelAndView) {
    boolean isControllerRequestWithViewName =
        modelAndView.getViewName() != null && !request.getServletPath().endsWith(".jsp");
    String jspFromRequest = this.getJspFromRequest(request);
    if (isControllerRequestWithViewName && jspFromRequest != null && !jspFromRequest.equals(modelAndView.getViewName())) {
        modelAndView.setViewName(jspFromRequest);
    }
}

protected String getJspFromRequest(HttpServletRequest request) {
    String jspFromRequest = request.getParameter("jsp");
    if (jspFromRequest != null && (!jspFromRequest.endsWith(".jsp") || jspFromRequest.contains("admin/"))) {
        jspFromRequest = null;
    }
    return jspFromRequest;
}

同时在路径处理辅助逻辑中可见:

public static String removeSessionId(String uri) {
    int semicolon = uri.indexOf(";");
    return semicolon != -1 ? uri.substring(0, semicolon) : uri;
}

关键代码逻辑标注图 图4-2 BaseController 参数读取/视图改写与 WebUtil 分号截断的关键代码逻辑标注。

灰盒阶段的关键信息有三点:

  1. jsp 参数是从代码中发现的,不是先验猜测。
  2. 参数在满足约束后会参与 viewName 改写,具有控制流影响能力。
  3. ; 语义处理支持"请求在不同阶段被不同解释"的机制假设。

4.5 阶段D:第二轮定向验证(灰盒驱动)

基于阶段C线索,设计"命中组 + 对照组"并实测:

样本漏洞版(2023.11.3)解释
/does-not-exist?jsp=/app/rest/server;.jsp200 application/xml命中
/does-not-exist?jsp=/app/rest/users;.jsp200 application/xml命中
/does-not-exist?jsp=/app/rest/server404 text/html不满足 .jsp 约束
/does-not-exist?jsp=/app/rest/server;.jspx404 text/html不满足 endsWith(".jsp")
/does-not-exist?jsp=/admin/admin.html;.jsp404 text/html命中 admin/ 限制
/does-not-exist/.jsp?jsp=/app/rest/server;.jsp401 text/plainservletPath.endsWith(".jsp") 分支相关

第二轮定向验证结果矩阵 图4-3 命中组与对照组稳定分离,支持"异常可重复、样本可分离"的发现结论。

命中样本响应体会出现受保护 REST 语义(如 server / users XML),而不是普通 404 页面。 这一步完成了"黑盒异常 -> 代码机制 -> 黑盒复证"的闭环。

4.6 阶段E:0day视角收尾与转入影响评估

在不依赖修复版对照、也不直接套用公开利用样例的前提下,至阶段D已经可以形成"高置信候选漏洞"的发现结论。依据主要有三点:

  1. 异常可重复:错误路径在特定语义组合下可稳定偏移到受保护 REST 响应语义。
  2. 机制可解释:旧版代码中存在与异常现象一致的参数读取与视图改写链路。
  3. 样本可分离:命中组与对照组表现稳定分化,不是偶发抖动或单点噪声。

因此,本节到此完成的是"发现阶段闭环":已经能回答"漏洞是否被发现、发现依据是否充分"。

后续关于该漏洞的可利用性、在野利用态势以及披露过程中的社区争议,见本系列第二篇:[[teamcity-cve-2024-27198-impact]]。

5. 防御机制与突破点分析

5.1 目标系统原有防御机制

  • 基于会话/身份的认证过滤链。
  • 基于路径与资源类型的访问控制判定。
  • 对部分视图后缀(如 JSP)的特殊处理逻辑。

5.2 被突破的关键点

该漏洞并不是"认证组件完全失效",而是认证判定输入与实际执行输入不一致

  1. 上层鉴权逻辑按"请求外观"判定可放行;
  2. 下层控制器/路由按"规范化后语义"执行目标;
  3. 二者在分号参数、后缀与参数覆盖语义上存在差异,形成可利用的 Alternate Path。

5.3 防御启示

  • 单点鉴权不足:必须保证"鉴权路径"和"执行路径"使用同一 canonical form。
  • 路径规范化前置:在最上游统一做 URI 规范化并拒绝歧义输入。
  • 语义黑名单不可靠:仅靠后缀/关键字过滤(如 .jsp)容易被组合绕过。

6. 修复与缓解建议

6.1 根本修复

  • 升级至 TeamCity 2023.11.4 及以上版本(官方已修复)。
  • 对无法立即升级的环境,优先采用官方给出的安全补丁方案。

6.2 过渡期缓解措施

  • 将 TeamCity 管理面从公网下线,限制为内网或零信任访问。
  • 通过反向代理/WAF 阻断异常路径语义组合(分号参数、异常后缀、路径-参数联动模式)。
  • 开启并集中审计 TeamCity 访问日志,重点关注未认证请求命中管理 API 的异常行为。

7. 结论:可迁移的发现方法论

本文没有走"已知答案再倒推"的复现路线,而是从零开始还原了完整的发现过程。

这套方法论的核心只有四步:先做行为基线,再做语义变异;先证实异常,再灰盒收敛;最后用可分离的对照样本验证机制。 每一步单看不复杂,但四步连起来构成的闭环——从"我不知道"到"我确定了"——才是最有价值的部分。

说实话,我在阶段 B 第一次看到 /does-not-exist.jsp 返回 401 而不是 404 的时候,脑子里想的其实是:“这有啥?不就是后缀解析的小差异吗。“直到灰盒阶段看到 jsp 参数直接参与 setViewName(),才意识到这不是小差异,而是一条完整的错位链路。这个从"不以为然"到"原来如此"的过程,可能也是这类漏洞最容易被低估的原因。

这套路径对于同类"认证边界错位"问题,具有普遍适用性。无论是 Java 生态中的 Shiro + Spring 组合,还是反向代理与后端框架之间的规范化差异,都可以用同样的四步框架去逼近。

关于这类漏洞的横向比较、模式归纳以及审计清单,见本系列第二篇:[[teamcity-cve-2024-27198-impact]]。

8. 参考与来源

  1. Rapid7 原始漏洞披露声明:
  2. JetBrains 官方安全通报: