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的分区;用于并行发送和接收消息
image-20230821081957570

漏洞分析

CVE-2023-33246漏洞对应的补丁地址如下: https://github.com/apache/rocketmq/commit/c469a60dcca616b077caf2867b64582795ff8bfc

补丁中删除了对应FilterServer的全部内容,初步判断应该是FilterServer存在漏洞。代码跟进到src/main/java/org/apache/rocketmq/broker/BrokerStartup类中,该类为Broker启动的代码

image-20230821082905437

main方法把createBrokerController方法返回的结果作为参数传入到start方法当中,其中createBrokerController创建了一个BrokerController返回。来到BrokerStartup#start()

image-20230821083149588

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

image-20230821082539226

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吧。

image-20230821084325985

这里就是很明显的RCE了,并且在调用exec前的splitShellString方法也只是通过空格去分割

private static String[] splitShellString(final String shellString) {
    return shellString.split(" ");
}

这时候回到buildStartCommand方法

image-20230821084559031

这里的逻辑就比较清晰了,最终会根据不同的操作系统,把``和在前面获取到的config拼接到最后的字符串当中。现在的问题就是要找到我们的可控变量。

阅读代码可以知道,config主要的内容是配置文件名和配置中的NamesrvAddr,这里选择在this.brokerController.getBrokerConfig().getRocketmqHome()获取到我们的payload。其实这个洞整体并不难分析,有意思的是具体最后RCE的payload怎么去构造。上文说到,在调用exec前会通过空格进行分割成一个数组,这里看到一个网上的绕过方法就是用@,payloadhttps://github.com/I5N0rth/CVE202333246@,具体payload可以看`https://github.com/I5N0rth/CVE-2023-33246`,@表示给脚本或命令的所有参数。

本地就不具体演示了,有兴趣的师傅可以自己搭建进行验证。