漏洞条件
- Apache Tomcat 6.x
- Apache Tomcat 7 < 7.0.100
- Apache Tomcat 8 < 8.5.51
- Apache Tomcat 9 < 9.0.31
- 开启 AJP Connector(默认开启监听8009端口)
前置知识
Tomcat Connector(连接器)
Tomcat Connector是Tomcat与外部连接的通道,它使得Catalina能够接收来自外部的请求,传递给对应的Web应用程序处理,并返回请求的响应结果。
默认情况下,Tomcat配置了两个Connector,它们分别是HTTP Connector和AJP Connector:
- HTTP Connector:用于处理HTTP协议的请求(HTTP/1.1),默认监听地址为0.0.0.0:8080
- AJP Connector:用于处理AJP协议的请求(AJP/1.3),默认监听地址为0.0.0.0:8009
Tomcat组件相关的配置文件是在conf/server.xml
,配置文件中每一个元素都对应了Tomcat的一个组件(可以在配置文件中找到如下两项,配置了两个Connector组件):
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
...
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
HTTP Connector就是用来提供我们经常用到的HTTP Web服务。而AJP Connector,它使用的是AJP协议(Apache Jserv Protocol),AJP协议可以理解为HTTP协议的二进制性能优化版本,它能降低HTTP请求的处理成本,因此主要在需要集群、反向代理的场景被使用。
显然,浏览器只支持HTTP协议,并不能直接支持AJP协议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议(8009端口)给客户端访问,大致如下图所示:
Servlet(服务程序)
Servlet意为服务程序,也可简单理解为是一种用来处理网络请求的一套规范。主要作用是给上级容器(Tomcat) 提供doGet()
和doPost()
等方法,其生命周期实例化、初始化、调用、销毁受控于Tomcat容器。
Tomcat中Servlet的配置是在conf/web.xml
。Tomcat默认配置定义了两个servlet,分别为DefaultServlet
和JspServlet
:
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
...
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
所有的请求进入tomcat,都会流经servlet。如果没有匹配到任何应用指定的servlet,那么就会流到默认的servlet(即DefaultServlet
),而JspServlet
负责处理所有JSP文件的请求。
Tomcat内部处理请求流程
- 用户点击网页内容,请求被发送到本机端口8080,被Connector获得(Connector中的Processor用于封装Request,Adapter用于将封装好的Request交给Container)。
- Connector把该请求交给Container中的Engine来处理,并等待Engine的回应。
- Engine获得请求
localhost/test/index.jsp
,匹配所有的虚拟主机Host。 - Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求
/test/index.jsp
,匹配它所拥有的所有的Context。Host匹配到路径为/test
的Context(如果匹配不到就把该请求交给路径名为" "
的Context去处理)。 path="/test"
的Context获得请求/index.jsp
,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp
的Servlet,对应于JspServlet类(匹配不到指定Servlet的请求对应DefaultServlet类)。- Wrapper是最底层的容器,负责管理一个Servlet。构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的
doGet()
或doPost()
,执行业务逻辑、数据存储等程序。 - Context把执行完之后的HttpServletResponse对象返回给Host。
- Host把HttpServletResponse对象返回给Engine。
- Engine把HttpServletResponse对象返回Connector。
- Connector把HttpServletResponse对象返回给客户Browser。
漏洞原理
这个漏洞主要是通过AJP协议(8009端口)触发。正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走default servlet
(DefaultServlet),另一个走jsp servlet
(JspServlet),可导致的不同的漏洞。
文件读取漏洞走的是DefaultServlet,文件包含漏洞走的是JspServlet。
PoC:
YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
文件读取漏洞
通过构造AJP协议请求,我们可以读取到 我们以读取WEB-INF/web.xml
文件为例。
POC中赋值了四个很重要的参数,先在此说明:
# 请求url
req_uri = '/asdf'
# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'WEB-INF/web.xml'
javax.servlet.include.servlet_path = '/'
**关键点1:AjpProcessor类
-> service()
-> **prepareRequest()
根据上文的Tomcat处理请求流程,请求首先到达Connector,Connector内使用AjpProcessor
解析Socket,将Socket中的内容封装到Request中。
所以我们首先将断点打到AjpProcessor
类的service()
方法,然后发送PoC进入断点:
一步步请求,随后跟入prepareRequest()
方法。
该方法解析请求,将相关属性匹配到该request的属性里。在一个while
循环分别将3个参数赋值(v赋值给n),放到request中。
放到request对象中的三个参数和对应参数值如下:
随后将请求传给CoyoteAdapter
:
在CoyoteAdapter
内部对request进行封装,将请求转发给Container:
随后直接走跟到ApplicationFilterChain
类的internalDoFilter()
方法将流程走到Servlet。
**关键点2:DefaultServlet类
-> service()
-> **doGet()
由上文介绍的Servlet
相关基础知识可知,该请求是非JSP文件请求,匹配不到指定的servlet,所以会映射到默认的servlet(default servlet
)处理。tomcat源码有个DefaultServlet
类(路径:org/apache/catalina/servlets/DefaultServlet.java
),我们断点也打到这个类,Debug看一下相关请求流程。
这里还要科普一下Servlet如何处理请求:一般请求到达servlet后先执行
service()
方法,在方法中根据请求方式决定执行doGet()
还是doPost()
方法。
流程进入DefaultServlet类
的service()
方法,这里调用其父类的service()
方法:
随后进入HttpServlet类
的service()
的方法,在这里调用了 doGet()
方法:
关键点3:****getRelativePath()
doGet()
方法内直接进入serveResource()
方法:
serveResource()
方法内首先是进入getRelativePath()
方法
该方法的作用是确认请求的资源路径,进入该方法,可以看到三个很重要的参数(红框):
这三个参数所对应的值为:
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";
与我们的POC中的三个赋值参数对应,POC中的参数代入getRelativePath()
方法,RequestDispatcher.INCLUDE_REQUEST_URI
的值为’/’,不为空。pathInfo和servletPath参数的值拼接成result,getRelativePath()
方法将result返回,返回内容为:’/WEB-INF/web.xml’。
**关键点4:getResource()
-> validate()
-> **normalize()
serveResource()
方法继续往下,看到调用了getResource()
方法:
跟入getResource()
方法,可以看到调用了validate()
方法:
validate()
方法内主要调用了normalize()
方法对path参数进行校验:
看normalize()
方法内做了那些校验:
这样一来如果想通过/WEB-INF/../../../../../etc/passwd
目录穿越的形式来读取文件是行不通的,因为当/../
在开头就会直接返回null,回到validate()
方法,就会报**IllegalArgumentException(非法参数)**的异常并终止本次操作。这导致了只能获取当前目录下(即webapps)的文件。
经过validate()
方法校验后,getResources()
方法随后的一系列操作就通过路径读取到了资源,并且为response的HTTP各种字段设置值。
关键点5:****ServletOutputStream.write()
最后通过getOutputStream()
方法获得ServletOutputStream
的实例:
使用resource.getContent()
获取资源内容,并利用ServletOutputStream.write()
向输出流写入返回内容。
随后再经过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。
因此可以尝试修改PoC,增加一个-w
参数,指定要读取的目录:
总结:至此,理解了如上6个关键点,整体漏洞流程也比较清晰了。
文件包含漏洞 (可致RCE)
理解了上文的文件读取漏洞的分析,接下来的内容很好理解。与上文不同的是,请求经过AjpProcessor
类的处理,随后将请求转发给了JspServlet
(该原理上文也有介绍,POC中的请求url是.jsp文件,而JspServlet
负责处理所有JSP文件的请求)。
首先在webapps/manager
目录下新建文件test.txt,内容为:
<%Runtime.getRuntime().exec("calc.exe");%>
修改POC进行调试。POC中的四个关键参数,也先在此说明:
# 请求url,这个参数一定要是以“.jsp”结尾
req_uri = '/manager/ddd.jsp'
# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'test.txt'
javax.servlet.include.servlet_path = '/'
**关键点1:JspServlet类
-> service()
-> **serviceJspFile()
断点打到JspServlet
类的service()
方法,先将servlet_path和path_info拼接在一起,赋值给jspUri(故这个参数是可控的)。
随后进入serviceJspFile()
方法,将/test.txt带入Tomcat加载和处理jsp的流程里。具体处理流程就不描述了,根据网上的一张图做了些修改,大致画了下Tomcat加载和处理jsp的流程图,能很清晰的看懂处理流程:
**关键点2:JspServletWrapper类
:getServlet()
-> **service()
最后返回到JspServletWrapper
类,获取jsp编译后生成的servlet,随后调用service()
方法,请求被执行。
**总结:**简单理解就是我们传入的”/test.txt”被当成jsp编译执行。带入了Tomcat处理jsp的处理流程,将jsp(test.txt
)转义成Servlet源代码.java(test_txt.java
),将Servlet源代码.java编译成Servlet类.class(test_txt.class
),Servlet类执行后,响应结果至客户端。
该漏洞造成RCE的条件是:在webapps目录下上传文件(可以是任意文件),随后通过该文件包含漏洞,造成RCE。
漏洞利用
文件读取
尝试读取刚才写到manage目录下的test.txt,注意请求URL最后不能为 .jsp结尾,才能走到DefaultServlet导致文件读取
文件包含
接着我们去执行test.txt,这时候需要请求的URL为 .jsp结尾,才能走到JspServlet导致文件包含
参考资料
Apache Tomcat 曝出 Ghostcat 高危文件读取/包含漏洞
Apache Tomcat 远程文件包含漏洞深入分析 by 天融信阿尔法实验室
CVE-2020-1938:Tomcat AJP文件包含漏洞分析 by c0ny1
不调试源码重现 Ghostcat 漏洞 (CVE-2020-1938) by xax007
Busting Ghostcat: An Analysis of the Apache Tomcat Vulnerability (CVE-2020-1938 and CNVD-2020-10487) by 趋势科技