Apache RocketMQ-CVE-2023-33246
RocketMQ介绍
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题
实现高性能,高可用,可伸缩和最终一致性架构。使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ,这边着重介绍RocketMQ的搭建和使用。RocketMQ是阿里研发的一个队列模型的消息中间件,后开源给apache基金会成为了apache的顶级开源项目,具有高性能、高可靠、高实时、分布式特点。
RocketMQ角色介绍
- Producer:消息的发送者;举例:发信者
- Consumer:消息接收者;举例:收信者
- Broker:暂存和传输消息;举例:邮局
- NameServer:管理Broker;举例:各个邮局的管理机构
- Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息
- Message Queue:相当于是Topic的分区;用于并行发送和接收消息
漏洞分析
CVE-2023-33246漏洞对应的补丁地址如下: https://github.com/apache/rocketmq/commit/c469a60dcca616b077caf2867b64582795ff8bfc
补丁中删除了对应FilterServer的全部内容,初步判断应该是FilterServer存在漏洞。代码跟进到src/main/java/org/apache/rocketmq/broker/BrokerStartup
类中,该类为Broker启动的代码
main方法把createBrokerController方法返回的结果作为参数传入到start方法当中,其中createBrokerController创建了一个BrokerController返回。来到BrokerStartup#start()
BrokerStartup#start()
先是调用上文提到的BrokerStartup的start方法,随后仅仅只是拼接了一些字符串和调用了类的getter方法,打印到日志当中。着重看start方法。
public void start() throws Exception {
if (this.messageStore != null) {
this.messageStore.start();
}
.......
.......
.......
if (this.filterServerManager != null) {
this.filterServerManager.start();
}
.......
.......
.......
}
在其中找到了如上的与filterServer有关的操作,调用了filterServerManager#start
,实际上除了调用了filterServerManager#start
以外,还调用了其他角色的start
方法,比如remotingServer
,继续跟进filterServerManager#start
在filterServerManager#start
中会间隔一段时间调用FilterServerManager.this.createFilterServer();
,跟进这个方法,对应代码如下
public void createFilterServer() {
int more = this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
String cmd = this.buildStartCommand();
for (int i = 0; i < more; i++) {
FilterServerUtil.callShell(cmd, log);
}
}
敏锐的师傅会发现他会去调用callShell
,并且参数还是通过调用buildStartCommand
方法获取的cmd
参数,这里就很像构造一个命令去执行。我们这里先来看callShell
吧。
这里就是很明显的RCE了,并且在调用exec
前的splitShellString
方法也只是通过空格去分割
private static String[] splitShellString(final String shellString) {
return shellString.split(" ");
}
这时候回到buildStartCommand
方法
这里的逻辑就比较清晰了,最终会根据不同的操作系统,把``和在前面获取到的config拼接到最后的字符串当中。现在的问题就是要找到我们的可控变量。
阅读代码可以知道,config
主要的内容是配置文件名和配置中的NamesrvAddr
,这里选择在this.brokerController.getBrokerConfig().getRocketmqHome()
获取到我们的payload。其实这个洞整体并不难分析,有意思的是具体最后RCE的payload怎么去构造。上文说到,在调用exec
前会通过空格进行分割成一个数组,这里看到一个网上的绕过方法就是用@表示给脚本或命令的所有参数。
本地就不具体演示了,有兴趣的师傅可以自己搭建进行验证。