Tomcat AJP 文件包含漏洞(CVE-2020-1938)
[toc]
漏洞概述
CVE-2020-1938 又名GhostCat
ApacheTomcat服务器中被发现存在文件包含漏洞,攻击者可利用该漏洞读取或包含Tomcat 上所有 webapp 目录下的任意文件
该漏洞是一个单独的文件包含漏洞,依赖于 Tomcat 的 AJP(定向包协议)。AJP 自身存在一定缺陷,由于Tomcat在处理AJP请求时,未对请求做任何验证,通过设置AJP连接器封装的request对象的属性, 导致产生任意文件读取漏洞和代码执行漏洞
影响范围
1 | Tomcat 9.x < 9.0.31 |
环境搭建
Tomcat 9.0.30 服务器
该漏洞修复版本为9.0.31,这里选用9.0.30
在server.xml中使用默认配置开启ajp端口8009
Apache 2.4.43 反向代理
作反代,/cat路由代理到8009端口上
漏洞分析
AJP Connector
Apache Tomcat服务器通过Connector连接器组件与客户程序建立连接,Connector表示接收请求并返回响应的端点。即Connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。在Apache Tomcat服务器中我们平时用的最多的8080端口,就是所谓的Http Connector,使用Http(HTTP/1.1)协议
在conf/server.xml文件里,他对应的配置为
1 | <Connector port="8080" protocol="HTTP/1.1" |
而 AJP Connector,它使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。
Ajp协议对应的配置为
1 | <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> |
Tomcat服务器默认对外网开启该端口 Web客户访问Tomcat服务器的两种方式:
AJP协议
AJP协议全称为 Apache JServ Protocol 目前最新的版本为 1.3
AJP协议是一个二进制的TCP传输协议,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。因为是二进制协议,所以浏览器并不能直接支持 AJP13 协议
向apache反代(192.168.142.129)发送一个请求
在wireshark中可以捕捉到反代(192.168.142.129)向tomcat(192.168.142.1)发送的ajp数据包
本问重点分析与本次漏洞有关的 AJP13_FORWARD_REQUEST
请求格式, 分析 wireshark 抓取到的数据包后理解格式并构造特定数据包进行漏洞利用,通过分析数据化分析出由以下几个部分组成
AJP MAGIC (1234)
AJP DATA LENGTH
AJP DATA
AJP END (ff)
在wireshark中将ajp的REQ:GET请求以十六进制导出
1 | 1234 # 前四个字节 1234 为 AJP MAGIC |
将最后面的两行数据十六进制导出
1 | AJP_REMOTE_PORT: 1551 |
比如此处的这一行请求头,由以下几部分组成
1 | 0a00 # request_header的标志 |
Tomcat 源码分析
这个漏洞的触发关系到的tomcat的很多个模块,这里选取几个重要的函数
prepareRequest()
Tomcat在接收ajp请求的时候调用org.apache.coyote.ajp.AjpProcessor来处理ajp消息,
prepareRequest()方法的大致功能是把将ajp里的内容取出,并设置成request对象的属性。
这个函数的流程大致如下,而漏洞出现在Decode extra attributes这一步当中
1 | private void prepareRequest() { |
Decode extra attributes会通过switch case来判断请求头的字段名称是不是预先定义好的,如果是预先定义的直接设置request对象的对应属性即可,如果不是预先定义好的会生成键值对添加到attributes中,这也是我们利用的地方
internalMapWrapper()
后续tomcat会进入Adapter中,调用Mapper类对request进行解析(这里调了很久才找到,对tomcat结构不熟悉),并选择合适的Wrapper
进入internalMapWrapper函数,这个函数共有五个匹配规则,为不同的uri选择合适的wrapper
1 | // Rule 1 -- Exact Match |
比如:
访问/:如果匹配到了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来接管这个请求
DefaultServlet.service()
如果进入了DefaultServlet,service函数会调用doGet,而doGet中会利用serveResource读取文件内容
接着进入getRelativePath()函数获取文件路径,当我们能控制INCLUDE_REQUEST_URI
,INCLUDE_PATH_INFO
,INCLUDE_SERVLET_PATH
这三个参数时就可以控制path
1 | String servletPath; |
最后在resources.getResource中读取文件内容
JspServlet.service()
在JspServlet中有一段对于jspUri的处理
1 | String jspUri = jspFile; |
如果我们能控制INCLUDE_SERVLET_PATH
和INCLUDE_PATH_INFO
就能控制这里的jspUri
后续再调用serviceJspFile来解析jsp文件,最后把结果返回
利用方式
读文件
发送如下数据包
访问的uri为/111,这样可以进入DefaultServlet,然后控制三个参数即可,最后读取的uri是/1.txt
这里并不能任意文件读,因为getResource中会经过合法性检验,反斜杠替换为斜杠,不是斜杠开头则添加一个,两个斜杠则去掉一个,是否有..之类等等,总之就是不能跨目录读取,其次就是修正一下不合规URI
执行jsp代码
当我们能够进行文件上传时,可以传一个1.txt
接着控制uri为一个jsp文件,不论是否存在
修复方式
官方在9.0.31中采取了以下的修复方式
Change the default bind address for AJP to the loopback address
Rename requiredSecret to secret and add secretRequired
1 | AJP Connector will not start if secretRequired="true" and secret is set |
- Add new AJP attribute allowedArbitraryRequestAttributes
1 | Requests with unrecognised attributes will be blocked with a 403 |
Reference
不调试源码重现 Ghostcat 漏洞 (CVE-2020-1938) (seebug.org)
CVE-2020-1938 : Tomcat-Ajp 协议漏洞分析-安全客 - 安全资讯平台 (anquanke.com)
https://www.anquanke.com/post/id/199448#h3-3