Apache Tomcat 从文件包含到RCE漏洞原理深入分析


Apache Tomcat 从文件包含到RCE漏洞原理深入分析

漏洞简介

2020年02月20日,于CNVD公开的漏洞公告中发现Apache Tomcat文件包含漏洞(CVE-2020-1938)。

Apache Tomcat为Apache开源组织开发的用于处理HTTP服务的项目。Apache Tomcat服务器中被发现存在文件包含漏洞,攻击者可利用该漏洞读取或包含 Tomcat 上所有 webapp 目录下的任意文件。

本次漏洞是一个单独的文件包含漏洞,该漏洞依赖于Tomcat的AJP(定向包协议)协议。AJP协议自身存在一定的缺陷,导致存在可控参数,通过可控参数可以导致文件包含漏洞。AJP协议使用率约为7.8%,鉴于Tomcat作为中间件被大范围部署在服务器上,本次漏洞危害较大。

AJP13协议介绍

我们对Tomcat的普遍认识主要有两大功能,一是充当web服务器,可以对一切静态资源的请求作出回应,二就是Servlet容器

常见的web服务器有 Apache、 Nginx、 IIS等。常见的Servlet容器有Tomcat,Weblogic,JBOSS等

Servlet容器可以理解为是Web服务器的升级版,拿Tomcat来举例,Tomcat本身可以不做Servlet容器使用,仅仅充当Web服务器的角色是完全没问题的,但是在处理静态资源请求的效率和速度上是远不及Apache,所以很多情况下生产环境中都会将Apache作为web服务器来接受用户的请求,静态资源有Apache直接处理,而Servlet请求则交由Tomcat来进行处理。这么做就可以让两个中间件各司其职,大大加快相应速度。

众所周知我们用户的请求是以http协议的形式传递给Web 服务器的,我们在浏览器中对某个域名或者ip进行访问,头部都会有http或者https的表示,而AJP协议浏览器是不支持的,我们无法通过浏览器发送AJP的报文。当然AJP这个协议也不是提供给我们用户来使用的。

在Tomcat$ CATALINA_BASE/conf/web.xml默认配置了两个Connector,分别监听两个不同的端口,一个是HTTP Connector 默认监听8080端口,一个是AJP Connector 默认监听8009端口。

HTTP Connector的主要就是负责接收来自用户的请求,不管事静态还是动态,只要是HTTP请求就时由HTTP Connector来负责。有了这个 Connector Tomcat才能成为一个web服务器,但还额外可处理Servlet和jsp。

而AJP协议的使用对象通常是另一个Web服务器。例如Apache ,这里从网上找到了一张图,以此图来进行说明

1

通常情况下AJP协议的使用场景是这样的。

AJP是一个二进制的TCP传输协议,浏览器无法使用,首先由Apahche与Tomcat之间进行AJP协议的通信,然后由Apache通过proxy_ajp模块进行反向代理,将其转换成HTTP服务器然后在暴露给用户,让用户来进行访问。

之所以要这么做,是因为相比HTTP这种纯文本的协议来说,效率和性能更高,同时也做了很多优化。

其实AJP协议某种程度上可以理解为是HTTP的二进制版,为了加快传输效率从而被使用,实际情况是像Apache这样有proxy_ajp模块可以反向代理AJP协议的很少,所以日常生产中AJP协议也很少被用到

Tomcat 远程文件包含漏洞分析

漏洞分析环境搭建

首先从官网下载对应的Tomcat源码文件,和可执行文件。

http://archive.apache.org/dist/tomcat/tomcat-8/v8.0.50/

2

下载好后将两个文件夹放入同一个目录下

然后在源码中新增pom.xml并加入以下内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

 <modelVersion>4.0.0</modelVersion>
 <groupId>org.apache.tomcat</groupId>
 <artifactId>Tomcat8.0</artifactId>
 <name>Tomcat8.0</name>
 <version>8.0</version>

 <build>
 <finalName>Tomcat8.0</finalName>
 <sourceDirectory>java</sourceDirectory>
 <testSourceDirectory>test</testSourceDirectory>
 <resources>
 <resource>
 <directory>java</directory>
 </resource>
 </resources>
 <testResources>
 <testResource>
 <directory>test</directory>
 </testResource>
 </testResources>
 <plugins>
 <plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>2.3</version>
 <configuration>
  <encoding>UTF-8</encoding>
  <source>1.8</source>
  <target>1.8</target>
 </configuration>
 </plugin>
 </plugins>
 </build>

 <dependencies>
 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>4.12</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>org.easymock</groupId>
 <artifactId>easymock</artifactId>
 <version>3.4</version>
 </dependency>
 <dependency>
 <groupId>ant</groupId>
 <artifactId>ant</artifactId>
 <version>1.7.0</version>
 </dependency>
 <dependency>
 <groupId>wsdl4j</groupId>
 <artifactId>wsdl4j</artifactId>
 <version>1.6.2</version>
 </dependency>
 <dependency>
 <groupId>javax.xml</groupId>
 <artifactId>jaxrpc</artifactId>
 <version>1.1</version>
 </dependency>
 <dependency>
 <groupId>org.eclipse.jdt.core.compiler</groupId>
 <artifactId>ecj</artifactId>
 <version>4.5.1</version>
 </dependency>
 </dependencies>
</project>

然后添加一个Application

3

1、按照下面图示新增Application的配置信息

2、在Man class:中填入:org.apache.catalina.startup.Bootstrap

3、在VM options:中填入:-Dcatalina.home="apache-tomcat-8.5.34",catalina.home替换成tomcat binary core的目录

4、jdk默认是1.8,因为我装的就是jdk1.8版本

5、启动过程中Test模块会报错,且为TestCookieFilter.java,注释里面的测试内容即可

然后运行 访问127.0.0.1:8080出现以下页面则环境搭建成功

4

漏洞复现

任意文件读取

5

6

RCE

7

8

漏洞分析

首先根据网上的介绍我们定位到 org.apache.coyote.ajp.AjpProcessor这个类,根据网上透漏的漏洞消息,我们得知漏洞的产生是由于Tomcat对ajp传递过来的数据的处理存在问题,导致我们可以控制“javax.servlet.include.request_uri”,“javax.servlet.include.path_info”,“javax.servlet.include.servlet_path”,这三个参数,从而读取任意文件,甚至可以进行RCE。

我们先从任意文件读取开始分析

我所使用的环境使用Tomcat 8.0.50版本所搭建的,产生漏洞的点并不在AjpProcessor.prepareRequest()方法,8.0.50版本的漏洞点存在于AjpProcessor的父类,AbstractAjpProcessor这个抽象类的prepareRequest()中

9

我们在这里下断点

然后运行exp,然后先看一下此时的调用链

10

首先由于此次数据传输使用的是AJP协议,走的的8009口,并非我们常见的HTTP协议。所以首先SocketPeocessore这个内部类来进行处理,

处理完成后经过几次调用交由AbstractAjpProcessor.prepareRequest(),该方法就是漏洞产生的第一个点。

11

我们单步步入request.setAttribute()方法

12

13

这里我们可以看到,attributes是一个HashMap,那这样就非常好理解了,就是将我们通过AJP协议传递过来的三个参数循环遍历存入这个HashMap

14

可以看到这里是一个while循环,我们来直接看循环完成后的结果

15

执行完后就会在Request对象的attributes属性中增加这三条数据。

到这里就是漏洞的前半部分,操纵可控变量将其改造层我们想要的数据。

我们先看一下exp发出的数据包是什么样的

16

我们通过使用WireShark抓包,看到了AJP报文的一些信息,其中有四个比较重要的参数,

URI:/asdf

javax.servlet.include.request_uri:/

javax.servlet.include.path_info: WEB-INF/Test.txt

javax.servlet.include.servlet_path:/

首先要讲到的就是这个URL,通过之前对AJP协议的介绍,我们知道通过AJP协议传来的数据最中还是要交由Servlet来进行处理的,那么问题就来了,应该交由那个Servlet来进行处理?

我们通过翻阅网上关于Tomcat的架构的一些文章和资料得知,在Tomcat$ CATALINA_BASE/conf/web.xml这个配置文件中默认定义了两个Servlet

一个是DefaultServlet

17

另一个是JspServlet

18

由于$ CATALINA_BASE/conf/web.xml这个文件是tomcat启动时默认加载的,所以这两个Servlet会默认存在Servlet容器中

当我们请求的URI不能和任何Servlet匹配时,就会默认由 DefaultServlet来处理,DefaultServlet主要用于处理静态资源,如HTML、图片、CSS、JS文件等,而且为了提升服务器性能,Tomcat对访问文件进行缓存。按照默认配置,客户端请求路径与资源的物理路径是一致的。

我们看到我们请求的URI为“/asdf”这就符合了无法匹配后台任何的Servlet这么一个条件,这里要注意一下,举个例子,譬如我们请求一个“abc.jsp” 但是后台没有“abc.jsp” 这种不属于无法匹配任何Servlet,因为.jsp的请求会默认走JspServlet来进行处理

19

好的,根据这段介绍,结合我们发送的数据包中的“URI:/asdf”这一属性,我们可以判断此次请求是由DefaultServlet来进行处理的。

我们定位到DefaultServlet的doGet方法

20

doGet方法里又调用了serveResource()方法

serveResource()方法由调用了getRelativePath()方法来进行路径拼接,我们跟进看一看

21

这里就是将我们传入的path_info 、servlet_path 进行复制的地方,request_uri用来做判断,如果发送的数据包中没有request_uri,就会走else后面的两行代码进行赋值,这样会就会导致漏洞利用失败

22

接下来就是对路径的拼接了,这里可以看到如果传递数据时不传递servlet_path,则result在进行路径拼接时就不会讲“/”拼接在“WEB-INF/web.xml”的头部,最后拼接的结果仍然是“WEB-INF/web.xml

23

接下来返回DefaultServle.serveResource()

紧接着判断path变量长度是否为0,为0则调用目录重定向方法

24

下面的代码就要开始读区我们指定的资源文件了

25

26

我们跟进StandardRoot.getResource()方法

27

getResource()方法中又调用了一个很重要的方法validate()方法并将path作为变量传递进去进行处理,我们继续跟入

这里就牵扯到为什么我们为什么不能通过”/../../“的方式来读取webapp目录的上层目录里文件的原因了,首先是正常请求

28

我们可以看到正常请求最后return的result的路径就是我们文件所在的相对路径。

当我门尝试使用WEB-INF/../../Test.txt来读区webapp意外的目录中的文件时。可以看到此时返回的result就是null了,而且会抛出异常。

29

这一切的原因都在RequestUtil.normalize()这个函数对我们传递进来的路径处理上,我们跟进看一看

关键的点就在下面的截图代码中。我们传入的路径是“/WEB-INF/../../Test.txt”,首先程序会判断我们的路径中是否存在“/../”,自然是存在的且索引是8大于,所以第一个if 判断不会成功,也就不会跳出while循环,此时处理我们的路径,截取“/WEB-INF/..”以后的内容,然后在用String,indexOf()函数判断路径里是否有“/../”,显然还是有的,且索引为零,符合第二个if判断的条件,return null。

30

此处的目的就是 不允许,不允许传递的路径的开头为“/../”,且不允许同时出现两个连在一起的“/../” 所以我们最多只能读取到webapp目录,无法读取webapp以外的目录中的文件。

想要读取webapp目录下的其余目录内的文件可以通过修改数据包中的”URI”这个参数来实现

31

如此一来,程序最中拼接出我们所指定文件的绝对路径,并作为返回值进行返回

32

接下来就是回到getResource()函数进行文件读取了

33

以下是任意文件读取的调用链

34

RCE

接下来讲一下,RCE实现的原理

之前讲过Tomcat$ CATALINA_BASE/conf/web.xml这个配置文件中默认定义了两个Servlet,刚才任意文件读取利用了DefaultServlet,而RCE就需要用到另一个也就是JspServlet

默认情况下,JspServlet的url-pattern为.jsp和.jspx,因此他负责处理所有JSP文件的请求。

JspServlet主要完成以下工作:

1.根据JSP文件生成对应Servlet的Java代码(JSP文件生成类的父类我org.apache.jasper.runtime.HttpJspBase——实现了Servlet接口)

2.将Java代码编译为Java类。

3.构造Servlet类实例并且执行请求。

其实本质核心就是通过JspServlet来执行我们想要访问的.jsp文件

所以想要RCE的前提就是,先要想办法将写有自己想要执行的命令的文件(可以是任意文件后缀,甚至没有后缀)上传到webapp的目录下,才能访问该文件然后通过JSP模板的解析造成RCE

来看下我们这次发送的Ajp报文的内容

35

这里的“URI”参数一定要是以“.jsp”进行结尾的,这个jsp文件可以不存在。

剩下的三个参数就和之前没什么区别了,“path_info”参数对应的就是我们上传的还有jsp代码的文件。

我们定位到JspServlet.Service()方法

36

可以看到首先将”servlet_path”的值取出赋值给变量jspUri

37

然后将”path_info”参数对应的值取出并赋值给“pathInfo”变量,然后和“jspUri”进行拼接

38

39

接下来跟进serviceJspFile()方法

40

首先生成JspServletWrapper对象

41

然后调用JspServletWrapper.service()方法

42

获取对应的Servlet

43

调用该Servlet的service方法

44

接下来就是就是解析我们上传文件中的java代码了至此,RCE漏洞原理分析完毕。下面是调用链

45


在法律允许的范围内, 有关部门临时工 在天融信阿尔法实验室工作期间创作 《Apache Tomcat 从文件包含到RCE漏洞原理深入分析》 。此作品发布在: 天融信阿尔法实验室官方公众号及其各大论坛官方账号

 上一篇
Padding Oracle原理深度解析 Padding Oracle原理深度解析
Padding的含义是“填充”,在解密时,如果算法发现解密后得到的结果,它的填充方式不符合规则,那么表示输入数据有问题,对于解密的类库来说,往往便会抛出一个异常,提示Padding不正确。
2020-05-31 有关部门临时工
下一篇 
CVE-2019-19781远程代码执行漏洞深入分析 CVE-2019-19781远程代码执行漏洞深入分析
CVE-2019-19781远程代码执行漏洞深入分析
2020-05-31 有关部门临时工
  目录