编号:CVE-2020-1938/CNVD-2020-10487
细节:Tomcat服务器存在文件包含漏洞,攻击者可利用该漏洞读取或包含Tomcat上所有webapp目录下的任意文件,如:webapp配置文件或源代码等。
POC:https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
Tomcat在部署时有两个重要的配置文件conf/server.xml
、conf/web.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" />
与此对应,tomcat启动后会监听8080、8009端口,它们分别负责接受http、ajp协议的数据。后者则和普通的java Web应用一样,用来定义servlet,这里是tomcat内建的几个servlet:
<!-- 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 (defined either here or in your own --> <!-- web.xml file). --> <servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> **** SNIP **** <load-on-startup>1</load-on-startup> </servlet> <!-- 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 (defined either here or in your own --> <!-- web.xml file). --> <servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> **** SNIP **** <load-on-startup>3</load-on-startup> </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>
就像注解中描述的default servlet用来处理所有未被匹配到其他servlet的uri请求,jsp servlet用来处理以.jsp、.jspxz做后缀名的uri请求,这俩都随tomcat一起启动。
tomcat的整体架构如上图所示,一个tomcat就是一个server,其中可以包含多个service(这里指是一个抽象的逻辑层)。而每个service由Connector、Container、Jsp引擎、日志等组件构成,与此次漏洞相关的组件主要是前两者。
Connector前面已经说过,是用来接受客户端的请求,请求中的数据包在被Connector解析后就会由Container处理。这个过程大致如下图:
Container中可以包含多个Host(虚拟主机,同Apache中定义),一个Host对应一个域名,因此Tomcat也可以配置多域名;每个Host又可以有多个Context,每个context其实就是一个web应用;而context下又有多个Wrapper,wrapper和servlet一一对应,只是它封装了一些管理servlet的函数。更进一步,客户端请求就交由servlet进入应用级的处理逻辑。
tomcat默认监听的8009端口用来处理AJP协议。AJP协议建立在TCP socket通信之上,tomcat使用该协议和前级的Web Server传递信息,这次的漏洞就出在客户端可以利用ajp协议数据包控制request对象的一些字段。
具体地,tomcat源码的org.apache.coyote.ajp.AjpProcessor
类的service()
方法如下:
它调用的prepareRequest()
方法用来解析一些请求头,部分内容如下:
可以看到,当ajp数据包的头设置为SC_REQ_ATTRIBUTE
时(具体数值可以查询AJP协议规范),Connector会紧接着读取变量n
(属性名)和v
(值),当n
不是SC_A_REQ_LOCAL_ADDR
、SC_A_REQ_REMOTE_PORT
、SC_A_SSL_PROTOCOL
时,就会用v
来赋值属性n
。接着,service()
方法将修改过的request代入后面的调用。
前面提到,当请求的uri无法匹配其他servlet时会由DefaultServlet处理,其中的调用流程如下图所示:
在org.apache.catalina.servlets.DefaultServlet
中,当我们的请求声明的是GET方法时,存在调用service()->doGet()->serveResource()
,分析serveResource()
代码如下:
其调用的getRelativePath()
方法内容如下:
protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) { String servletPath; String pathInfo; if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); } else{ ...... } StringBuilder result = new StringBuilder(); if (servletPath.length() > 0) { result.append(servletPath); } if (pathInfo != null) { result.append(pathInfo); } ...... return result.toString(); }
从javax.servlet.RequestDispatcher
中可以看到这三个属性的名称:
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";
所以,我们就能通过AJP协议改变request的这三个属性来控制请求的路径,serveResource()
方法获得path后的代码大致如下:
/* * serveResource()方法后续 */ ...... WebResource resource = resources.getResource(path); ...... ServletOutputStream ostream = null; ostream = response.getOutputStream(); ...... byte[] resourceBody = resource.getContent(); if (resourceBody == null) { ...... } else { // Use the resource content directly ostream.write(resourceBody); } ......
它会直接把通过path获取的资源序列化输出,因此客户端再按照AJP协议解析数据包就能得到文件内容。
同样的道理,tomcat默认将jsp/jspx结尾的请求交给org.apache.jasper.servlet.JspServlet
处理,它的service()
方法如下:
可以看到jspUri也是由两个可控的属性定义的,而在后续的代码中将会把jspUri指向的资源当做jsp文件执行,这就造成了文件包含漏洞。当Web应用上有某个文件内容可被我们控制时,譬如某应用头像上传后的存储路径位于webapp目录中,就能构造RCE。
在复现时我在一台内网的虚拟机上搭建了Tomcat8.5.40,IP是10.0.12.93。然后下载了github上别人的POC,稍微修改了里面的代码,使它支持jsp-servlet的利用方式。首先测试任意文件下载漏洞,结果如下:
然后测试文件包含漏洞,我在webapps/examples/
新建一个文件evilman.png
,当做通过某接口上传的头像:
其中只有一句jsp:
<% out.println(new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec("whoami").getInputStream())).readLine()); %>
最后,验证结果如下:
conf/server.xml
文件中8009的Connector注释掉