文章首发先知社区:https://xz.aliyun.com/t/7683 (知乎专栏的排版真是难用,先知的阅读体验会更好一些,在意排版的可移步先知社区)。
0x00 写在前面
前几天仔细跟了下这个漏洞,学到了很多东西,尽量详细地对漏洞分析和相关基础知识做了记录。
相关知识点用最通俗易懂的方式进行描述,应该能清晰的明白漏洞原理。
零基础慎入,因为一不小心你就看懂了。
0x01 环境搭建
以tomcat 8.5.46版本为例进行漏洞分析,首先下载tomcat源码:http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.46/src/apache-tomcat-8.5.46-src.zip。
搭建过程可以参考这篇Paper:Tomcat源码编译(IDEA)_Java_ww0peo的博客-CSDN博客,跟着这篇Paper一步一步搭建完成后,运行,随后浏览器访问http://127.0.0.1:8080会报500错误:
![87096f10de5b8f969b5f8c5ac921b99d.png](https://i-blog.csdnimg.cn/blog_migrate/8d1449522875a40a7b780ebab6b17846.jpeg)
解决办法是IDEA中找到 org.apache.catalina.startup.ContextConfig ,增加如下的一行代码,将JSP解析器初始化:
context
![d403b845d58cbcad0604aed81a70a452.png](https://i-blog.csdnimg.cn/blog_migrate/b970281443fb74b5d00a7bb8be19839b.jpeg)
随后再次启动Tomcat,浏览器就能正常看到Tomcat的主页了。查看端口开放的开放情况,Tomcat运行开启了8009和8080端口。
0x02 基础简介
(1) Tomcat Connector(连接器)
首先来说一下Tomcat的Connector组件,Connector组件的主要职责就是负责接收客户端连接和客户端请求的处理加工。每个Connector会监听一个指定端口,分别负责对请求报文的解析和响应报文组装,解析过程封装Request对象,而组装过程封装Response对象。
举个例子,如果把Tomcat比作一个城堡,那么Connector组件就是城堡的城门,为进出城堡的人们提供通道。当然,可能有多个城门,每个城门代表不同的通道。而Tomcat默认配置启动,开了两个城门(通道):一个是监听8080端口的HTTP Connector,另一个是监听8009端口的AJP Connector。
Tomcat组件相关的配置文件是在 conf/server.xml ,配置文件中每一个元素都对应了Tomcat的一个组件(可以在配置文件中找到如下两项,配置了两个Connector组件):
<!-- Define a non-SSL/TLS HTTP/1.1 Connector on port 8080 -->
HTTP Connector很好理解,通过浏览器访问Tomcat服务器的Web应用时,使用的就是这个连接器;
AJP Connector是通过AJP协议和一个Web容器进行交互。在将Tomcat与其他HTTP服务器(一般是Apache )集成时,就需要用到这个连接器。AJP协议是采用二进制形式代替文本形式传输,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。
显然,浏览器只支持HTTP协议,并不能直接支持AJP协议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议(8009端口)给客户端访问,大致如下图所示:
![68bac9ef9ec37652aafb81f8f9ec4fd7.png](https://i-blog.csdnimg.cn/blog_migrate/2eadb7f94c5b51908bc9de439a30cd5c.png)
(2) Servlet(服务程序)
Servlet意为服务程序,也可简单理解为是一种用来处理网络请求的一套规范。主要作用是给上级容器(Tomcat)提供doGet()和doPost()等方法,其生命周期实例化、初始化、调用、销毁受控于Tomcat容器。有个例子可以很好理解:想象一下,在一栋大楼里有非常多特殊服务者Servlet,这栋大楼有一套智能系统帮助接待顾客引导他们去所需的服务提供者(Servlet)那接受服务。这里顾客就是一个个请求,特殊服务者就是Servlet,而这套智能系统就是Tomcat容器。
Tomcat中Servlet的配置是在 conf/web.xml 。Tomcat默认配置定义了两个servlet,分别为 DefaultServlet 和 JspServlet :
<!-- The default servlet for all web applications, that serves static -->
所有的请求进入tomcat,都会流经servlet。由注释可以很明显看出,如果没有匹配到任何应用指定的servlet,那么就会流到默认的servlet(即 DefaultServlet ),而 JspServlet 负责处理所有JSP文件的请求。
(3) Tomcat内部处理请求流程
Tomcat内部处理请求的流程第一次看可能觉得会有点复杂。网上很多分析tomcat内部架构的文章,看几篇就能明白个大概了。网上看到张图,简单修改重新绘制了下,介绍一下Tomcat内部处理HTTP请求的流程,便于理解后续的漏洞分析:
![cb4cbb9b4d985eff16c1d3a643990f47.png](https://i-blog.csdnimg.cn/blog_migrate/86fb70e389239b4c9db2c7aa636dca3a.jpeg)
1. 用户点击网页内容,请求被发送到本机端口8080,被Connector获得(Connector中的Processor用于封装Request,Adapter用于将封装好的Request交给Container)。
2. Connector把该请求交给Container中的Engine来处理,并等待Engine的回应。
3. Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
4. Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为" "的Context去处理)。
5. path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类(匹配不到指定Servlet的请求对应DefaultServlet类)。
6. Wrapper是最底层的容器,负责管理一个Servlet。构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等程序。
7. Context把执行完之后的HttpServletResponse对象返回给Host。
8. Host把HttpServletResponse对象返回给Engine。
9. Engine把HttpServletResponse对象返回Connector。
10. Connector把HttpServletResponse对象返回给客户Browser。
0x03 漏洞分析
理解了上文的基础,下面开始分析漏洞。这个漏洞主要是通过AJP协议(8009端口)触发。正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走 default servlet (DefaultServlet),另一个走 jsp servlet (JspServlet),可导致的不同的漏洞。
文件读取漏洞走的是DefaultServlet,文件包含漏洞走的是JspServlet。
下面开始逐一进行分析,测试使用的POC如下:
YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
(1) 文件读取漏洞
通过构造AJP协议请求,我们可以读取到 我们以读取 WEB-INF/web.xml 文件为例。
POC中赋值了四个很重要的参数,先在此说明:
# 请求url
关键点1:AjpProcessor类 -> service() -> prepareRequest()
根据上文的Tomcat处理请求流程,请求首先到达Connector,Connector内使用 AjpProcessor 解析Socket,将Socket中的内容封装到Request中。
所以我们首先将断点打到 AjpProcessor 类的 service() 方法:
![4cc1aab4fba6f69db5dee700f3650d4c.png](https://i-blog.csdnimg.cn/blog_migrate/f3127f7e45359a31f0f412dea6aec770.jpeg)
一步步请求,随后跟入 prepareRequest() 方法。该方法解析请求,将相关属性匹配到该request的属性里。重点看这里:
![88336a82a4020f5d05db5892391dab2d.png](https://i-blog.csdnimg.cn/blog_migrate/1af10517297b1c41cf435906fe420182.jpeg)
放到request对象中的三个参数和对应参数值如下:
![62b674b7c71126c11f70558f5f76f285.png](https://i-blog.csdnimg.cn/blog_migrate/9841fe18fab4eeeed40d8e8066a8892b.jpeg)
随后将请求传给 CoyoteAdapter ,对request进行封装,将请求抓发给Container:
![c4d60bfea84ce0f1a84fd979ed431296.png](https://i-blog.csdnimg.cn/blog_migrate/93be1cf67dea2bfdd9037e8ecec161e2.jpeg)
随后的Tomcat内部处理流程跳过,直接看Servlet中的处理,调用栈很清晰的展现了Tomcat内部处理的流程:
![d9f765792c7595724740a1a8a5729292.png](https://i-blog.csdnimg.cn/blog_migrate/d9dc6e9abe04323d6a7e1af602c54bbf.jpeg)
最后通过 ApplicationFilterChain 类的 internalDoFilter() 方法将流程走到Servlet。
![ec5475455f492434d9840cf1e62bc265.png](https://i-blog.csdnimg.cn/blog_migrate/1a7c7a673eb9ab6439d0bdfd72e414e8.jpeg)
关键点2:DefaultServlet类 -> service() -> doGet()
由上文介绍的 Servlet 相关基础知识可知,该请求是非JSP文件请求,匹配不到指定的servlet,所以会映射到默认的servlet( default servlet )处理。tomcat源码有个 DefaultServlet 类(路径: org/apache/catalina/servlets/DefaultServlet.java ),我们断点也打到这个类,Debug看一下相关请求流程。
> 这里还要科普一下Servlet如何处理请求:一般请求到达 servlet 后先执行 service() 方法,在方法中根据请求方式决定执行 doGet() 还是 doPost() 方法。
流程进入 service() 方法,随后进入 doGet() 方法:
![f70874547806fe0552f104144e6c8499.png](https://i-blog.csdnimg.cn/blog_migrate/c39ac5d23c2b2eb74b1c9c9f0b7a7a2b.jpeg)
关键点3:getRelativePath()
doGet() 方法内直接进入 serveResource() 方法,我们直接看 serveResource() 方法:
![9d4e50561ef9cfeb1158b71d9fa247a0.png](https://i-blog.csdnimg.cn/blog_migrate/835077dd6ee45b077ff3512ab1bd99b2.jpeg)
首先是进入 getRelativePath() 方法,该方法的作用是确认请求的资源路径,进入该方法,可以看到三个很重要的参数(红框):
![52c4d791f3c23e1222e409e7857a5494.png](https://i-blog.csdnimg.cn/blog_migrate/0548f0da9dde0e1c435049f8d2e3b742.jpeg)
这三个参数所对应的值为:
static
与我们的POC中的三个赋值参数对应,POC中的参数代入 getRelativePath() 方法, RequestDispatcher.INCLUDE_REQUEST_URI 的值为'/',不为空。pathInfo和servletPath参数的值拼接成result, getRelativePath() 方法将result返回,返回内容为: /WEB-INF/web.xml 。
关键点4:getResource() -> validate() -> normalize()
serveResource() 方法继续往下,可以看到这行代码:
// path的值就是getRelativePath()方法的返回值:'/WEB-INF/web.xml'
跟入 getResource() 方法,可以看到调用了 validate() 方法。
![c96d6ecedf58a50b23a91b9c1e184033.png](https://i-blog.csdnimg.cn/blog_migrate/e9afe2e7235dcd0c916454a5ee0a2bac.jpeg)
validate() 方法内主要调用了 normalize() 方法对path参数进行校验。
result
我们直接看 normalize() 方法内做了那些校验:
![45021559aa7dfb32cee3e74b5f0b2bf8.png](https://i-blog.csdnimg.cn/blog_migrate/85e62e0fc88f6d4b73d3b5045d81552a.jpeg)
返回null,回到 validate() 方法,就会报IllegalArgumentException(非法参数)的异常并终止本次操作。所以,我们的请求路径中不能包含"/../",也就导致了该漏洞只能读取webapps目录下的文件。
经过 validate() 方法校验后,getResources() 方法随后的一系列操作就通过路径读取到了资源。
关键点5:ServletOutputStream.write()
最后通过 getOutputStream() 方法获得 ServletOutputStream 的实例:
![ddc062d9af2b3989d4756b3c27603105.png](https://i-blog.csdnimg.cn/blog_migrate/98f2334e313bec78f851465cad89116f.jpeg)
利用 ServletOutputStream.write() 向输出流写入返回内容。
![cdad2aa477f1205d2bd3ae862c4233d6.png](https://i-blog.csdnimg.cn/blog_migrate/da35f8659e1425f6219a99472c671294.jpeg)
随后再经过Tomcat内部流程处理,经过Tomcat的 Container 和 Connector ,最终返回给客户端。
关键点6:POC中的请求url(读取webapps下其他目录的文件)
前文提到POC中还有个关键参数 req_uri ,这个参数的设置决定了我们可以读取webapps下其他目录的文件。设置其值为一个随意字符串'asdf',一来是无法匹配到webapps下的路径,走tomcat默认的ROOT目录;二来是为了让tomcat将请求流到 DefaultServlet ,从而触发漏洞。当请求读取 WEB-INF/web.xml 文件,则读取的就是 webapps/ROOT/WEB-INF/ 目录下的web.xml。
当读取 webapps/manager 目录下的文件,只需修改POC中 req_uri 参数为'manager/asdf',读取 WEB-INF/web.xml 文件则是读取 webapps/manager/WEB-INF/ 目录下的web.xml。
总结:至此,理解了如上6个关键点,整体漏洞流程也比较清晰了。
漏洞复现:修改POC中的请求url为 /manager/asdf ,发送POC,读取到 webapps/manager/status.xsd 文件的内容(POC有做修改):
![2194075e9218c7f85d21d5614faa7a6b.png](https://i-blog.csdnimg.cn/blog_migrate/226186f2b562827bcfe16e8d8722ef93.jpeg)
(2) 文件包含漏洞 (可致RCE)
理解了上文的文件读取漏洞的分析,接下来的内容很好理解。与上文不同的是,请求经过 AjpProcessor 类的处理,随后将请求转发给了 JspServlet (该原理上文也有介绍,POC中的请求url是.jsp文件,而 JspServlet 负责处理所有JSP文件的请求)。
首先在 webapps/manager 目录下新建文件test.txt,内容为:
<%
修改POC进行调试。POC中的四个关键参数,也先在此说明:
# 请求url,这个参数一定要是以“.jsp”结尾
关键点1:JspServlet类 -> service() -> serviceJspFile()
断点打到 JspServlet 类的 service() 方法,先将servlet_path和path_info拼接在一起,赋值给jspUri(故这个参数是可控的)。
![0cfaa8fa1c6e3f18099b1e82dcd97906.png](https://i-blog.csdnimg.cn/blog_migrate/2c19f7586725d76ee700203a71cc080c.jpeg)
随后进入 serviceJspFile() 方法,将/test.txt带入Tomcat加载和处理jsp的流程里。具体处理流程就不描述了,根据网上的一张图做了些修改,大致画了下Tomcat加载和处理jsp的流程图,能很清晰的看懂处理流程:
![6d571a3e669a62e02e99e462c1a50ccc.png](https://i-blog.csdnimg.cn/blog_migrate/81a1cbe42a5790c327fe4cf37293c616.jpeg)
关键点2:JspServletWrapper类:getServlet() -> service()
最后返回到 JspServletWrapper 类,获取jsp编译后生成的servlet,随后调用service()方法,请求被执行。
![fc302e5e46609f6f73adbccd9b2e9aec.png](https://i-blog.csdnimg.cn/blog_migrate/65a7c38d09c6b082a285c1c8593a47d5.jpeg)
总结:简单理解就是我们传入的"/test.txt"被当成jsp编译执行。带入了Tomcat处理jsp的处理流程,将jsp( test.txt )转化成Servlet源代码.java( test_txt.java ),将Servlet源代码.java编译成Servlet类.class( test_txt.class ),Servlet类执行后,响应结果至客户端。
![1d774cea2c07fd55ab16fe25a4a5c459.png](https://i-blog.csdnimg.cn/blog_migrate/a6ee80e2611c8950e5822bf9581285fa.png)
该漏洞造成RCE的条件是:在webapps目录下上传文件(可以是任意文件),随后通过该文件包含漏洞,造成RCE。
漏洞复现:修改poc中的请求url为 manager/ddd.jsp ,test.txt中的代码被执行。
![e392893ab785a767875066c22dd8f36e.png](https://i-blog.csdnimg.cn/blog_migrate/e68a6d16dad511945834c0e724000e4b.jpeg)
0x04 漏洞修复
以官方发布的9.0.31版本的修复代码为例,主要做了以下修复:
- 默认在conf/server.xml中禁用AJP连接器;
2. 强制AJP协议默认监听本地环回地址,而不是0.0.0.0;
3. 若使用AJP协议,设置secretRequired属性为true,强制配置secret来设置AJP协议认证凭证;
![4d470759127ed93cad0992acd6f708d8.png](https://i-blog.csdnimg.cn/blog_migrate/7fc77f2cb99ebbc2127e53d8a08eaab2.jpeg)
4. 配置属性白名单,若向AJP连接器发送任意未被识别的属性,都会响应403;
![12b5f51c12237487e94f6cf9553db3aa.png](https://i-blog.csdnimg.cn/blog_migrate/9e62fe80d6115de37ee2b8d58b21484d.jpeg)
0x05 参考文章
- Tomcat内核详解(六):Connector组件_java_qq_36807862的博客-CSDN博客;
2. Tomcat整体架构浅析_Java_新博客地址:https://my.oschina.net/hebaodan/blog/-CSDN博客;
3. 解析Tomcat内部结构和请求过程 - 逝宇、 - 博客园;
4. Apache Tomcat 远程文件包含漏洞深入分析 by 天融信阿尔法实验室;
5. Tomcat Ajp协议文件包含漏洞分析 by d00ms;
6. CVE-2020-1938:Tomcat AJP文件包含漏洞分析 by c0ny1;
7. 不调试源码重现 Ghostcat 漏洞 (CVE-2020-1938) by xax007;
8. Busting Ghostcat: An Analysis of the Apache Tomcat Vulnerability (CVE-2020-1938 and CNVD-2020-10487) - by 趋势科技;