Yoga7xm's Blog

Tomcat LFI(CVE-2020-1938) 漏洞分析

字数统计: 2.5k阅读时长: 12 min
2020/02/24 Share

Abstract

Tomcat AJP协议由于存在实现缺陷导致相关参数可控,攻击者利用该漏洞可通过构造特定参数,读取服务器webapp下的任意文件。若服务器端同时存在文件上传功能,攻击者可进一步实现远程代码的执行

——《关于Apache Tomcat存在文件包含漏洞的安全公告》

影响版本:

  • 6.0
  • 7.0 < 7.0.100
  • 8.0 < 8.5.51
  • 9.0 < 9.0.31

环境搭建

  • tomcat 8.5.47
  • IDEA2019

下载该版本的源码文件➡传送门,然后解压进入该目录。新建一个pom.xml解决依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?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.0.2</version>

<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant-apache-log4j</artifactId>
<version>1.6.5</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant-commons-logging</artifactId>
<version>1.6.5</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml.rpc</groupId>
<artifactId>javax.xml.rpc-api</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>4.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

然后创建一个home的文件夹,将webapps和conf文件复制过去,再创建三个文件夹,最终的结构如下

IDEA导入pom.xml新建项目,然后开始下载依赖jar包

导入成功之后开始配置Configurations,新建一个Application,配置项如下:

  • 主类名:org.apache.catalina.startup.Bootstrap
  • 虚拟机选项:-Dcatalina.home=”home文件夹路径”

【注】:如果类名报错,就选中项目中java文件夹标记为src

运行即可。BTW,我这里启动的时候报错:未能找到该CookieFilter

需要在test/util中新建这么一个类,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package util;

import java.util.Locale;
import java.util.StringTokenizer;

/**
* Processes a cookie header and attempts to obfuscate any cookie values that
* represent session IDs from other web applications. Since session cookie names
* are configurable, as are session ID lengths, this filter is not expected to
* be 100% effective.
*
* It is required that the examples web application is removed in security
* conscious environments as documented in the Security How-To. This filter is
* intended to reduce the impact of failing to follow that advice. A failure by
* this filter to obfuscate a session ID or similar value is not a security
* vulnerability. In such instances the vulnerability is the failure to remove
* the examples web application.
*/
public class CookieFilter {

private static final String OBFUSCATED = "[obfuscated]";

private CookieFilter() {
// Hide default constructor
}

public static String filter(String cookieHeader, String sessionId) {

StringBuilder sb = new StringBuilder(cookieHeader.length());

// Cookie name value pairs are ';' separated.
// Session IDs don't use ; in the value so don't worry about quoted
// values that contain ;
StringTokenizer st = new StringTokenizer(cookieHeader, ";");

boolean first = true;
while (st.hasMoreTokens()) {
if (first) {
first = false;
} else {
sb.append(';');
}
sb.append(filterNameValuePair(st.nextToken(), sessionId));
}


return sb.toString();
}

private static String filterNameValuePair(String input, String sessionId) {
int i = input.indexOf('=');
if (i == -1) {
return input;
}
String name = input.substring(0, i);
String value = input.substring(i + 1, input.length());

return name + "=" + filter(name, value, sessionId);
}

public static String filter(String cookieName, String cookieValue, String sessionId) {
if (cookieName.toLowerCase(Locale.ENGLISH).contains("jsessionid") &&
(sessionId == null || !cookieValue.contains(sessionId))) {
cookieValue = OBFUSCATED;
}

return cookieValue;
}
}

再次运行,访问http://127.0.0.1:8080成功,表示环境搭建成功

Analysis

AJP协议

AJP13协议是定向包协议,出于性能考虑选择了更具可读性的纯文本的二进制格式。Web服务器通过TCP连接与Servlet容器进行通信,为了节省建立套接字的开销,采用长连接的形式,并且在多个请求/响应中重用连接。

我们来看conf\server.xml

Tomcat默认配置了两个connector

  • HTTP Connector:默认监听8080端口,负责建立HTTP连接,用于接收客户端发来的请求以及Tomcat返回的响应,同时
  • AJP Connector:默认监听8009端口,可以通过ajp协议与其他web服务器进行数据交互

客户端访问Tomcat服务器的两种方式(图来源):

复现

POC1 POC2

读取/ROOT/WEB-INF/2.txt文件内容

抓包查看过程。具体的协议分析可以参考Tomcat官方文档传送门

这个数据包的重点在于以下三个选项:

1
2
3
javax.servlet.include.request_uri: /
javax.servlet.include.path_info: /WEB-INF/2.txt
javax.servlet.include.servlet_path: /

漏洞分析

AJP2HTTP

定位到src\java\org\apache\coyote\ajp\AjpProcessor.java的prepareRequest方法中

拿到Request请求中的methodCode(2),然后解析成GET请求,紧接着就是解析Req请求拿到协议、URI、Host等信息

下面就进入一个循环然后进入case逻辑用来解析AJP协议未定义的请求头,显然之前协议包中设置的三个属性也会放到request中

然后回到AjpProcessor#service方法,调用适配器来处理Req和Resp对象

进入CoyoteAdapter#service()中,对连接器设置一系列属性之后,再利用反射去调用Servlet处理

DefaultServlet

从AJP拿到的是Get请求,所以会调用Servlet的doGet()方法处理

1
2
3
4
5
6
7
8
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {

// Serve the requested resource, including the data content
serveResource(request, response, true, fileEncoding);

}

跟进这个serveResource方法

调用getRelativePath方法从Request中确定请求的资源路径,我们来看下这方法

通过拼接PATH_INFO+SERVLET_PATH = /WEB-INF/2.txt拿到该资源的相对路径,然后回到serveResource根据路径获取资源。其中会调用normalize方法来验证路径是否合法

这就限制了不能使用../进行跨目录,只能在Webapps目录下。在后续逻辑里,先生成响应头,然后getContent()拿到资源对象的内容作为响应Body返回给客户端

总调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
serveResource:1142, DefaultServlet (org.apache.catalina.servlets)
doGet:504, DefaultServlet (org.apache.catalina.servlets)
service:634, HttpServlet (javax.servlet.http)
service:484, DefaultServlet (org.apache.catalina.servlets)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:200, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:528, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:678, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:476, AjpProcessor (org.apache.coyote.ajp)
process:66, AbstractProcessorLight (org.apache.coyote)
process:810, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1500, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

虽然不能绕过webapps目录,但是通过修改访问的uri就能够读取任意war包目录下文件


JspServlet

如果请求的URL为jsp文件,就会在之前反射的时候调jspservlet处理

同样是用servlet_pathpath_info进行拼接拿到jspUri。然后经过一系列逻辑还是会走到getResource传入path去获取文件内容

同样会去判断是否存在../之类的字符,如果存在就返回path为NULL,最终抛出异常退出。拿到文件内容之后调用jsp引擎去解析,达到了RCE的目的。

总调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
_jspService:1, _1_txt (org.apache.jsp)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:741, HttpServlet (javax.servlet.http)
service:476, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:386, JspServlet (org.apache.jasper.servlet)
service:330, JspServlet (org.apache.jasper.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:493, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:137, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:476, AjpProcessor (org.apache.coyote.ajp)
process:66, AbstractProcessorLight (org.apache.coyote)
process:808, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1498, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

RCE的POC(需要上传至服务器,即使是图片也能成功)

1
<%Runtime.getRuntime().exec("calc.exe");%>

补丁

传送门

白名单限制可以赋值的设置,之前的三个设置不可控。

Defence

  • 升级打补丁
  • 关闭ajp、修改地址为127.0.0.1、设置高强度secret

Reference

https://zhuanlan.zhihu.com/p/29665847

https://www.cnblogs.com/softidea/p/5735102.html

http://gv7.me/articles/2020/cve-2020-1938-tomcat-ajp-lfi/

https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi

https://www.anquanke.com/post/id/199448

CATALOG
  1. 1. Abstract
  2. 2. 环境搭建
  3. 3. Analysis
    1. 3.1. AJP协议
    2. 3.2. 复现
    3. 3.3. 漏洞分析
    4. 3.4. 补丁
  4. 4. Defence
  5. 5. Reference