CVE-2023-37582
产品介绍
RocketMQ是阿里巴巴在2012年开发的分布式消息中间件,后捐献给Apache软件基金会并成为Apache的顶级项目。RocketMQ专为万亿级超大规模的消息处理而设计,具有高吞吐量、低延迟、海量堆积、顺序收发等特点。
漏洞概述
当RocketMQ的NameServer组件暴露在外网,并且缺乏有效的身份认证机制时,攻击者可以利用nameServer更新配置功能,以RocketMQ运行的系统用户身份进行任意文件上传,甚至实现远程代码执行。
受影响版本
<RocketMQ 4.9.7
<RocketMQ 5.1.2
漏洞分析
RocketMQ基本概念
要想对该漏洞有较为清晰的认识,我们首先要了解RocketMQ的基本架构以及各部分的功能。
RocketMQ主要由四部分组成:
| 概念 | 说明 |
|---|---|
| Producer | Prducer负责消息的生产,支持分布式集群部署,通过MQ的负载均衡系统选择相应的Broker集群进行消息的存储与投递。 |
| Consumer | Consumer负责消息的消费,支持分布式集群部署,同时支持通过PUSH与PULL的方式对消息进行消费。 |
| NameServer | NameServer是一个Topic路由注册中心,其角色类似于Dubbo中的ZooKeeper,Hadoop中的NameNode,负责整个系统元数据的存储与管理,支持Broker的动态注册与发现。其主要包含两个功能。一是Broker的注册与管理,其接受Broker的信息并保存作为路由的基本信息,并提供心跳机制来确保每一个Broker集群的有效性;二是路由信息的管理,NameServer存储了整个系统的Broker路由信息,Producer与Consumer通过NameSerVer便可进行消息的投递与消费。 |
| Broker | Broker主要负责消息的存储、投递与消费并保证服务的高可用。 |
下图为各组件的关联方式:

测试环境搭建
调试环境
访问项目官方仓库获取受影响漏洞版本源码(本文使用v4.9.4版本)
1 | https://github.com/apache/rocketmq/releases |
使用idea导入项目后需启动NameServer与Broker,同时为BrokerStarup与NamesrvStartup配置运行时环境变量ROCKETMQ_HOME,两者配置相同地址即可,为了让Broker能够找到Namesrv,Broker的配置中需要额外指定命令行参数-c namesrv_endpoint

ROCKETMQ_HOME为ROCKETMQ运行家目录,配置好环境变量后在ROCKETMQ_HOME指向的目录行新建文件夹conf,进入conf目录并新建文件logback_broker.xml与logback_namesrv.xml,将以下内容复制到文件中:
1 |
|
环境配置完成后运行主类org.apache.rocketmq.broker.NameSrvStartup启动ROCKETMQ NameServer,控制台输出以下内容则NameServer启动成功

运行主类org.apache.rocketmq.broker.BrokerStartup启动ROCKETMQ Broker,控制台输出以下内容则Broker启动成功

日志中提示了Broker监听地址与NameServer地址。
靶场搭建
调试环境可用于测试PoC
详情
为了更清晰地了解该漏洞,我们仍需了解NameServer的启动过程。另一方面,该漏洞是针对NameServer的攻击与Broker关系不大,了解NameServer的启动过程将更有助于我们分析漏洞。

在org.apache.rocketmq.namesrv.NamesrvStartup类中,启动阶段的关键代码出现在第57与58行。启动时首先要做的是创建NameSrvController进行初始化,然后再执行start流程。
创建Controller过程中主要进行了命令行参数解析、配置文件解析、日志配置解析并将相关的配置映射到注册namesrvConfig与nettyServerConfig两个对象中并向Controller注册

start启动过程中最终会调用到org.apache.rocketmq.remoting.netty.NettyRemotingServer的start方法


该方法主要完成了各类处理器的初始化、Netty解码器与消息处理器的注册、监听服务的启动。
其中encoder负责RocketMQ协议消息头的解析与消息体的提取,最终形成字节流传递给serverHandler进行处理。

serverHandler会根据不同的消息类型选择不同的处理方式

在进行Request类型消息处理时会先选择处理器类型,然后调用其processRequest方法进行具体的消息处理

默认使用的消息处理器类为org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor,其processRequest方法会根据请求中的code参数选择不同的处理方式。

在监听服务启动后,netty将按照每秒1次的频率不断获取监听端口信息,然后一次调用decoder、handler对消息进行处理,上面就是Namesrv的简单启动流程。
有趣的是当org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest 第88行request.getCode方法的结果为318时则进入到配置更新流程

在updateConfig方法中首先会获取请求体对象然后转换为字符串,然后将字符串转换为properties对象,最终将properties更新到configuration对象中

this.namesrvController.getConfiguration()获取到的是一个Configuration对象,然后调用其update方法进行配置更新

org.apache.rocketmq.common.Configuration#update方法首先将用户请求体的配置信息合并到this.allconfigs中,然后遍历configObjectList对象成员,再将配置信息合并到其中。从上图我们知道configObjectList包含两个对象分别是程序启动过程中对configuration初始化时传入的NamesrvConfig与NettyServerConfig对象。完成配置在内存态的更新后调用persist方法对配置进行持久化。

persist方法完成了在内存中实时的配置信息的持久化,其首先会调用getAllConfigsInternal方法获取所有配置,然后调用getStorePath方法获取到配置的存储路径,最终调用string2File方法以前两个要素为参数配置持久化到硬盘。

一般来讲,任意文件上传漏洞需要满足两个条件,一是文件的后缀名可控,二是文件的内容可控。对于该漏洞来说我们通过getAllConfigsInternal获取到文件内容,通过getStorePath获取到文件路径。那么这两者是否可控呢?
我们来看getAllConfigsInternal方法,该方法主要做了两件事,一是遍历configObjectList对象然后将其成员转换为properties对象,二是将properties对象序列化为字符串。从前面我们知道configObjectList对象可以被用户通过code为318的请求修改,也就是说文件内容是可控的。

再看getStorePath方法,在第156行有很明显的反射调用,获取的值是NamesrvConfig对象中configStorePath字段的值。

在update方法中185行我们知道,用户请求体中的配置会修改NamesrvConfig的属性,故持久化的配置文件的存储路径也是可变的,于是便构成了任意文件上传的两个要素。
我们使用互联网流传的PoC对漏洞进行验证,因为本文的复现环境为Windows所以需要对PoC进行适当的修改,可以看到在PoC中,用户发送给NameServer的请求体数据为两个通过换行符分割的键值对类型数据,分别为configStorePath与productEnvName,其中configStorePath负责指定配置文件的存储路径,productEnvName只是用于验证配置是否写入成功。

执行验证脚本

到启动目录查看发现文件写入成功

若需要进行命令执行只需要向启动目录里写入bat文件即可

或者向.ssh文件夹下的authorized_keys中写入新的公钥(未验证)
最后放上调用栈
1 | persist:208, Configuration (org.apache.rocketmq.common) |
修复措施
1、非必须勿向互联网开放不必要的服务端口,控制暴露面;
2、关键服务增加用户权限校验;
3、升级软件到不受影响的版本或最新版。
参考链接
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37582
- https://www.cvedetails.com/cve-details.php?t=1&cve_id=CVE-2023-37582
- https://www.cnnvd.org.cn/home/globalSearch?keyword=CVE-2023-37582