[toc]

img

漏洞概述

CVE-2020-1938 又名GhostCat

ApacheTomcat服务器中被发现存在文件包含漏洞,攻击者可利用该漏洞读取或包含Tomcat 上所有 webapp 目录下的任意文件

该漏洞是一个单独的文件包含漏洞,依赖于 Tomcat 的 AJP(定向包协议)。AJP 自身存在一定缺陷,由于Tomcat在处理AJP请求时,未对请求做任何验证,通过设置AJP连接器封装的request对象的属性, 导致产生任意文件读取漏洞和代码执行漏洞

影响范围

1
2
3
4
Tomcat 9.x < 9.0.31
Tomcat 8.x < 8.5.51
Tomcat 7.x < 7.0.100
Tomcat 6.x

环境搭建

Tomcat 9.0.30 服务器

该漏洞修复版本为9.0.31,这里选用9.0.30

在server.xml中使用默认配置开启ajp端口8009

image-20221112160814278

Apache 2.4.43 反向代理

作反代,/cat路由代理到8009端口上

image-20221112160606916

漏洞分析

AJP Connector

Apache Tomcat服务器通过Connector连接器组件与客户程序建立连接,Connector表示接收请求并返回响应的端点。即Connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。在Apache Tomcat服务器中我们平时用的最多的8080端口,就是所谓的Http Connector,使用Http(HTTP/1.1)协议

在conf/server.xml文件里,他对应的配置为

1
2
3
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

而 AJP Connector,它使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。

Ajp协议对应的配置为

1
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

Tomcat服务器默认对外网开启该端口 Web客户访问Tomcat服务器的两种方式:

public_image

AJP协议

AJP协议全称为 Apache JServ Protocol 目前最新的版本为 1.3

AJP协议是一个二进制的TCP传输协议,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。因为是二进制协议,所以浏览器并不能直接支持 AJP13 协议

向apache反代(192.168.142.129)发送一个请求

image-20221113222704431

在wireshark中可以捕捉到反代(192.168.142.129)向tomcat(192.168.142.1)发送的ajp数据包

image-20221113222611999

本问重点分析与本次漏洞有关的 AJP13_FORWARD_REQUEST 请求格式, 分析 wireshark 抓取到的数据包后理解格式并构造特定数据包进行漏洞利用,通过分析数据化分析出由以下几个部分组成

  • AJP MAGIC (1234)

  • AJP DATA LENGTH

  • AJP DATA

  • AJP END (ff)

在wireshark中将ajp的REQ:GET请求以十六进制导出

1
2
3
4
1234 # 前四个字节 1234 为 AJP MAGIC
00ae # DATA LENGTH 段和 DATA 段总长度
02020008485454502f312e3100001d2f4356455f323031375f31323631355f7761725f6578706c6f6465642f00000d3139322e3136382e3134322e3100ffff000f3139322e3136382e3134322e313239001f40000001a00b00143139322e3136382e3134322e3132393a38303030000a000f414a505f52454d4f54455f504f525400000431353531000a000e414a505f4c4f43414c5f4144445200000f3139322e3136382e3134322e313239 # 数据段
00ff # 结束符

将最后面的两行数据十六进制导出

1
2
3
4
5
AJP_REMOTE_PORT: 1551
AJP_LOCAL_ADDR: 192.168.142.129

0a000f414a505f52454d4f54455f504f52540000043135353100
0a000e414a505f4c4f43414c5f4144445200000f3139322e3136382e3134322e31323900

比如此处的这一行请求头,由以下几部分组成

1
2
3
4
5
6
7
0a00 # request_header的标志
0f # 请求头名称的长度
414a505f52454d4f54455f504f5254 # 请求头名称'AJP_REMOTE_PORT'
0000 # 用来分割请求头名称和值
04 # 请求头值的长度
31353531 # 请求头的值'1551'
00 # 结束标志

image-20221113224801975

Tomcat 源码分析

这个漏洞的触发关系到的tomcat的很多个模块,这里选取几个重要的函数

prepareRequest()

Tomcat在接收ajp请求的时候调用org.apache.coyote.ajp.AjpProcessor来处理ajp消息,

prepareRequest()方法的大致功能是把将ajp里的内容取出,并设置成request对象的属性。

image-20221112162114630

这个函数的流程大致如下,而漏洞出现在Decode extra attributes这一步当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void prepareRequest() {

// Translate the HTTP method code to a String.
...
// Decode headers
...
// Set this every time in case limit has been changed via JMX
...
// Decode extra attributes
...
// Check if secret was submitted if required
...
// Check for a full URI (including protocol://host:port/)
...
}

Decode extra attributes会通过switch case来判断请求头的字段名称是不是预先定义好的,如果是预先定义的直接设置request对象的对应属性即可,如果不是预先定义好的会生成键值对添加到attributes中,这也是我们利用的地方

image-20221114015329973

internalMapWrapper()

后续tomcat会进入Adapter中,调用Mapper类对request进行解析(这里调了很久才找到,对tomcat结构不熟悉),并选择合适的Wrapper

image-20221114013525773

进入internalMapWrapper函数,这个函数共有五个匹配规则,为不同的uri选择合适的wrapper

1
2
3
4
5
6
7
8
// Rule 1 -- Exact Match
// Rule 2 -- Prefix Match
// Rule 3 -- Extension Match
// Rule 4 -- Welcome resources processing for servlets
// Rule 4a -- Welcome resources processing for exact macth
// Rule 4b -- Welcome resources processing for prefix match
// Rule 4c -- Welcome resources processing for physical folder
// Rule 7 -- Default servlet

比如:

访问/:如果匹配到了index.jsp会在Rule 4c中设置wrapper(org.apache.jasper.servlet.JspServlet)

访问/index.jsp:会在Rule 3中设置wrapper(org.apache.jasper.servlet.JspServlet)

访问/111:会在Rule 7中设置wrapper(org.apache.catalina.servlets.DefaultServlet)

设置好了wrapper后会初始化servlet,最后在HttpServlet中调用service,然后对应servlet来接管这个请求

image-20221114021724147

DefaultServlet.service()

如果进入了DefaultServlet,service函数会调用doGet,而doGet中会利用serveResource读取文件内容

image-20221114023254485

接着进入getRelativePath()函数获取文件路径,当我们能控制INCLUDE_REQUEST_URIINCLUDE_PATH_INFOINCLUDE_SERVLET_PATH这三个参数时就可以控制path

1
2
3
4
5
6
7
8
9
10
11
String servletPath;
String pathInfo;

if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
// For includes, get the info from the attributes
pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
} else {
pathInfo = request.getPathInfo();
servletPath = request.getServletPath();
}

最后在resources.getResource中读取文件内容

image-20221114023855633

JspServlet.service()

在JspServlet中有一段对于jspUri的处理

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
String jspUri = jspFile;

if (jspUri == null) {
/*
* Check to see if the requested JSP has been the target of a
* RequestDispatcher.include()
*/
jspUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH);
if (jspUri != null) {
/*
* Requested JSP has been target of
* RequestDispatcher.include(). Its path is assembled from the
* relevant javax.servlet.include.* request attributes
*/
String pathInfo = (String) request.getAttribute(
RequestDispatcher.INCLUDE_PATH_INFO);
if (pathInfo != null) {
jspUri += pathInfo;
}
} else {
/*
* Requested JSP has not been the target of a
* RequestDispatcher.include(). Reconstruct its path from the
* request's getServletPath() and getPathInfo()
*/
jspUri = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
jspUri += pathInfo;
}
}
}

如果我们能控制INCLUDE_SERVLET_PATHINCLUDE_PATH_INFO就能控制这里的jspUri

image-20221114022757028

后续再调用serviceJspFile来解析jsp文件,最后把结果返回

利用方式

读文件

发送如下数据包

image-20221114024440354

访问的uri为/111,这样可以进入DefaultServlet,然后控制三个参数即可,最后读取的uri是/1.txt

image-20221114024837432

这里并不能任意文件读,因为getResource中会经过合法性检验,反斜杠替换为斜杠,不是斜杠开头则添加一个,两个斜杠则去掉一个,是否有..之类等等,总之就是不能跨目录读取,其次就是修正一下不合规URI

执行jsp代码

当我们能够进行文件上传时,可以传一个1.txt

接着控制uri为一个jsp文件,不论是否存在

image-20221114025154763

image-20221114025125772

修复方式

官方在9.0.31中采取了以下的修复方式

  1. Change the default bind address for AJP to the loopback address

    image-20221114123403305

  2. Rename requiredSecret to secret and add secretRequired

1
2
AJP Connector will not start if secretRequired="true" and secret is set
to null or zero length String.

image-20221114123520634

  1. Add new AJP attribute allowedArbitraryRequestAttributes
1
Requests with unrecognised attributes will be blocked with a 403

image-20221114123626895

Reference

https://www.tenable.com/blog/cve-2020-1938-ghostcat-apache-tomcat-ajp-file-readinclusion-vulnerability-cnvd-2020-10487

不调试源码重现 Ghostcat 漏洞 (CVE-2020-1938) (seebug.org)

CVE-2020-1938 : Tomcat-Ajp 协议漏洞分析-安全客 - 安全资讯平台 (anquanke.com)

https://www.anquanke.com/post/id/199448#h3-3

Apache Tomcat® - Apache Tomcat 9 vulnerabilities

AJPv13 (apache.org)