前几天仔细跟了下这个漏洞,学到了很多东西,尽量详细地对漏洞分析和相关基础知识做了记录。
相关知识点用最通俗易懂的方式进行描述,应该能清晰的明白漏洞原理。
零基础慎入,因为一不小心你就看懂了。
以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),跟着这篇Paper一步一步搭建完成后,运行,随后浏览器访问http://127.0.0.1:8080会报500错误:
解决办法是IDEA中找到org.apache.catalina.startup.ContextConfig
,增加如下的一行代码,将JSP解析器初始化:
context.addServletContainerInitializer(new JasperInitializer(), null);
随后再次启动Tomcat,浏览器就能正常看到Tomcat的主页了。查看端口开放的开放情况,Tomcat运行开启了8009和8080端口。
首先来说一下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 --> <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很好理解,通过浏览器访问Tomcat服务器的Web应用时,使用的就是这个连接器;
AJP Connector是通过AJP协议和一个Web容器进行交互。在将Tomcat与其他HTTP服务器(一般是Apache )集成时,就需要用到这个连接器。AJP协议是采用二进制形式代替文本形式传输,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。
显然,浏览器只支持HTTP协议,并不能直接支持AJP协议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议(8009端口)给客户端访问,大致如下图所示:
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 --> <!-- resources. It processes all requests that are not mapped to other --> <!-- servlets with servlet mappings. --> <servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> ...... ...... </servlet> <!-- The JSP page compiler and execution servlet, which is the mechanism --> <!-- used by Tomcat to support JSP pages. Traditionally, this servlet --> <!-- is mapped to the URL pattern "*.jsp". --> <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 --> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> <url-pattern>*.jspx</url-pattern> </servlet-mapping>
所有的请求进入tomcat,都会流经servlet。由注释可以很明显看出,如果没有匹配到任何应用指定的servlet,那么就会流到默认的servlet(即DefaultServlet
),而JspServlet
负责处理所有JSP文件的请求。
Tomcat内部处理请求的流程第一次看可能觉得会有点复杂。网上很多分析tomcat内部架构的文章,看几篇就能明白个大概了。网上看到张图,简单修改重新绘制了下,介绍一下Tomcat内部处理HTTP请求的流程,便于理解后续的漏洞分析:
理解了上文的基础,下面开始分析漏洞。这个漏洞主要是通过AJP协议(8009端口)触发。正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走default servlet
(DefaultServlet),另一个走jsp servlet
(JspServlet),可导致的不同的漏洞。
文件读取漏洞走的是DefaultServlet,文件包含漏洞走的是JspServlet。
下面开始逐一进行分析,测试使用的POC如下:
https://github.com/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 = '/'
AjpProcessor类
-> service()
-> prepareRequest()
根据上文的Tomcat处理请求流程,请求首先到达Connector,Connector内使用AjpProcessor
解析Socket,将Socket中的内容封装到Request中。
所以我们首先将断点打到AjpProcessor
类的service()
方法:
一步步请求,随后跟入prepareRequest()
方法。该方法解析请求,将相关属性匹配到该request的属性里。重点看这里:
放到request对象中的三个参数和对应参数值如下:
随后将请求传给CoyoteAdapter
,对request进行封装,将请求抓发给Container:
随后的Tomcat内部处理流程跳过,直接看Servlet中的处理,调用栈很清晰的展现了Tomcat内部处理的流程:
最后通过ApplicationFilterChain
类的internalDoFilter()
方法将流程走到Servlet。
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()
方法:
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'。
getResource()
-> validate()
-> normalize()
serveResource()
方法继续往下,可以看到这行代码:
// path的值就是getRelativePath()方法的返回值:'/WEB-INF/web.xml' WebResource resource = resources.getResource(path);
跟入getResource()
方法,可以看到调用了validate()
方法。
validate()
方法内主要调用了normalize()
方法对path参数进行校验。
result = RequestUtil.normalize(path, true);
我们直接看normalize()
方法内做了那些校验:
返回null,回到validate()
方法,就会报IllegalArgumentException(非法参数)的异常并终止本次操作。所以,我们的请求路径中不能包含"/../",也就导致了该漏洞只能读取webapps目录下的文件。
经过validate()
方法校验后,getResources()
方法随后的一系列操作就通过路径读取到了资源。
ServletOutputStream.write()
最后通过getOutputStream()
方法获得ServletOutputStream
的实例:
利用ServletOutputStream.write()
向输出流写入返回内容。
随后再经过Tomcat内部流程处理,经过Tomcat的Container
和Connector
,最终返回给客户端。
前文提到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有做修改):
理解了上文的文件读取漏洞的分析,接下来的内容很好理解。与上文不同的是,请求经过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 = '/'
JspServlet类
-> service()
-> serviceJspFile()
断点打到JspServlet
类的service()
方法,先将servlet_path和path_info拼接在一起,赋值给jspUri(故这个参数是可控的)。
随后进入serviceJspFile()
方法,将/test.txt带入Tomcat加载和处理jsp的流程里。具体处理流程就不描述了,根据网上的一张图做了些修改,大致画了下Tomcat加载和处理jsp的流程图,能很清晰的看懂处理流程:
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。
漏洞复现:修改poc中的请求url为manager/ddd.jsp
,test.txt中的代码被执行。
以官方发布的9.0.31版本的修复代码为例,主要做了以下修复:
conf/server.xml
中禁用AJP连接器;