哈喽,EveryOne。本文将分享一个RocketMQ的漏洞从发现到自动化利用的实践过程,最后也将安利一个开源的神器Arthas(阿尔萨斯)。本文技术含金量不高,但也希望读者能从中有所收获。
一、介绍一下RocketMQ
RocketMQ是阿里贡献给apache的开源项目,地址在:https://github.com/apache/rocketmq,它是一种纯Java语言开发的消息中间件。也是国内首个非Hadoop生态体系的顶级项目,被国内外数百家企业广泛使用。具有:**高性能、低延迟、高可靠** 的特点。
RocketMQ的服务端由Name Server 和 Broker组成。消费者/生产者在使用RocketMQ做消息调度时,必须要指定Topic。Broker在整个RocketMQ中又担当着消息中枢的功能,Topic的创建和更新消息通过Broker转发到namesrv。
我们从官方下载rocketmq-all-4.6.1.zip 解压后打开到bin目录:
同时配置环境变量如下:
可以下载这个工具的jar包,然后用下面的命令,就可以很方便的管理RocketMQ了。【TIPS:RocketMQ 9876端口未授权访问,可以用这个工具很方便的利用哟。】
java -Xmx512m -Xms512m -jar rocketmq-console-ng-1.0.0.jar --server.port=8081
--rocketmq.config.namesrvAddr="10.13.32.203:9876" --auth.username=admin --auth.password=admin
RocketMQ的消息存储是由consume queue和commit log配合完成的。consume queue是消息的逻辑队列,相当于字典的目录,用来指定消息在物理文件commit log上的位置。当创建一个话题(Topic)时会创建一个独立的存储文件夹,官方定义是:
${rocketmq.home}/store/consumequeue/${topicName}/${queueId}/${fileName}
例如下面的:
生产者在使用RocketMQ进行创建消息时,必须要指定topic,对于topic的设置有一个开关autoCreateTopicEnable,一般在开发测试环境中会使用autoCreateTopicEnable = true(默认为true),官方建议在生产环境里需要手动设置autoCreateTopicEnable = false
当开启autoCreateTopicEnable后,我们的每一个topic在RocketMQ的服务端上都会创建一个独立的文件夹来存储 consume queue。
我们构造一个生产者来实践如下,具体的细节就不细说了。如下:
在Windows上我们看到存储消息的文件夹创建成功了,并且文件初始大小约为5.72M如下:
这个创建的路径为固定的:当前用户目录\store\consumequeue 这是所有消息存储的文件路径,更多关于这块的介绍可以看文档:
消息存储设计
二、猜测与攻击
黑客大多都是脑洞大开的,既然topic由生产者创建消息时指定的,哪么创建文件时会不会有漏洞呢?如果创建文件夹的参数为 ../../../fuzz2019
,即topic参数传值为../../../fuzz2019
,哪么服务端会发生什么呢??
构造Topic为 ../../../demo
试一试,发现结果如下:
红色提示
The specified topic[../../../fuzz2019] contains illegal characters, allowing only ^[%|a-zA-Z0-9_-]+$
看来是对topic的格式有要求,根据提示找到 过滤类 org.apache.rocketmq.client.Validators
错误信息提示了在86行这里的有对消息的topic做本地检查 Validators.checkTopic(msg.getTopic());
这里是被本地创建消息时被检查格式不合法给拒绝了,如果RocketMQ服务端没有做这个Topic的格式检查,信任了客户端的Topic,直接使用传入的Topic值进行了文件夹创建。这就可能存在漏洞,使用 ../ 可以目录穿越到其他任意目录。
为了验证上面的想法,于是需要客户端构造恶意的Topic值如:../../../fuzz2019
这里下面我想到有三种方式实现我们的目的:
使用IDE工具调试功能,通过下断点动态修改内存中Topic值。
下载源码修改检查逻辑后,编译新的rocketmq-client包导入使用。
使用JAVA Agent技术,运行时修改对象的值。
为了快速验证猜想,就通过在Eclipse上调试,动态修改内存中对象的topic的值为恶意的poc。
首先传一个正常的topic值,如:fuzz2019,当执行流程通过 Validators.checkTopic(msg.getTopic())的格式检查后,下断点,在IDE里直接修改msg对象的topic值为恶意的如:../../../../fuzz2019
如下所示
在Eclipse里,右键-Change Value
填入 payload
然后快速的放行,等待程序执行完成如下。
可以看到在C盘的根目录下成功创建一个名叫 fuzz2019的文件夹
或者穿越到其他目录下创建
至此,一个简单的构想,POC验证已经完成,证明RocketMQ服务端,未做topic的格式合法检查,仅在rocketmq-client端生产者请求时做了特殊字符判断,该漏洞存在~~
试着分析了一下产生的原因在于broker的处理类中:
broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java
这个文件的165行的msgCheck函数里,在对rocketmq-client发过来的消息检查时,如果topic不存在就会通过TopicConfigManager类调用createTopicInSendMessageMethod方法,将请求头里获取的topic直接用于创建topic.
TopicConfigManager类文件为 /broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java
这里我做了一些其他测试,结论:
不能覆盖已存在的文件夹,因为服务端检查会认为已经存在该文件夹,然后直接存放在其下面。
不能控制生成任意的文件和文件内容
到此关于这个漏洞的发现过程,产生原因已经描述清楚。哪么这个漏洞还能有什么更大的利用空间吗??我能想到的就是 利用目录穿越到系统敏感目录,fuzz创建带有特殊字符的文件夹,使系统core dump或某些服务的crash
三、如何更进一步利用
虽然这个漏洞不及反序列化导致RCE那样的严重危害,但是这个如果被恶意利用也是有一定安全隐患的。从官方设计文档 存储设计文档(https://github.com/apache/rocketmq/blob/master/docs/cn/design.md) 中关于存储架构中1.1节中的描述:
具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M
攻击者每发送一次网络请求,都可以在任意指定的目录下创建5.72M大小的文件夹。这个放大的比例应该是非常大了吧:1次请求换来消耗目标5.72M磁盘空间 这个是不是可以用作于放大攻击呢??攻击者可以写出EXP向服务器任意目录进行喷射,短时间内可以在任意目录下创建大量垃圾文件,消耗尽磁盘存储空间。
为了方便自动化攻击或方便前面提到的Fuzz,需要更好的方式解决传入../../这样的特殊符号。
下面将介绍第二种的方式: RASP中使用的JAVA Agent技术之 agentmain(热部署)。 agentmain是JDK6后引入的新的特性,agentmain 可以在类加载之后再次加载一个类,也就是重定义。更多的介绍可以看:
agentmain(https://www.jianshu.com/p/6096bfe19e41)
关于agentmain的原理就不介绍了,直接上手使用吧。经过debug,我知道关键的校验点在Validators类里的checkMessage函数,这里我直接注释掉所有的检查,只打印 当前传入的topic值。
编写好以后,保存,然后你就准备好了用于替换目标Class的字节码文件啦。。地址在这里: /Users/CFHB/DevTools/rocketmq-master/client/target/classes/org/apache/rocketmq/client/Validators.class
后面用到的代码都来自GitHub的Demo JavaHotSwap(https://github.com/Jason112788/JavaHotSwap/), 感谢GitHub上的无数无私的网友们,不然一天从入门到上手真的难。
拿到JavaAgent的Demo后,需要做的就是重写agentmain函数里的内部transform函数,这个的作用就是前面说的利用JDK提供的API,重定义已经加载的类。我这里需要这样做:
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("[transform] hotswap class name :" + className);
if ("org/apache/rocketmq/client/Validators".equalsIgnoreCase(className)){ //如果是目标类,则进行替换。
byte[] bytes = fileToBytes(new File("/Users/CFHB/DevTools/rocketmq-master/client/target/classes/org/apache/rocketmq/client/Validators.class"));
return bytes;
}else{
return null; // 返回null,表示不修改
}
简单理解,transform 是进行替换的,返回null则不做替换,返回字节数组,则进行替换。上面判断如果是目标类:
org/apache/rocketmq/client/Validators
就会进行替换,如果你要问为什么是/这样的?你先不替换,只打印出来className,看看格式就知道了。
至此,JavaAgent编写已完成,maven 命令打包一下吧。
maven clean
maven package
maven install
也就是这里的JavaHotSwap,这里就是给JVM加载agentmain.jar,传递目标类做参数:org.apache.rocketmq.client.Validators
上面的工作是利用热更新,动态改掉了rocket-client包里的检查逻辑,现在需要做的就是构造../../这样的请求啦。这里循环100次,每次sleep 2秒,代码如下:
至此,所有的东西已经准备好了,
直接攻击,被检查拦截
加载热更新,改掉检查逻辑
攻击成功效果:
ubuntu下被攻击后:
至此,介绍完毕利用JAVA Agent技术帮助Fuzz攻击的演示。
四、关于修复
RocketMQ在最新发布的4.6.1版本中已经完成了该漏洞的修复。在文件broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java
中将rocketmq-client客户端的检查逻辑,同样添加在了服务端。
推荐使用最新版本,RocketMQ 4.6.1(已修复)
rocketmq-client使用4.6.0以上版本(fastjson已升级到1.2.61)
禁止使用ROOT权限启用RocketMQ服务端和消费端
禁止自动创建topic,必须将autoCreateTopicEnableRocketMQ设置为false. 修改Broker配置文件,broker.properties,添加一项autoCreateTopicEnable=false.
配置ACL,开启用户身份认证。修改RocketMQ的默认端口9876为不常用端口,防止被扫描发现。
五、安利神器(Arthas)
Arthas(https://github.com/alibaba/arthas/blob/master/README\_CN.md)
Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到JVM的实时运行状态?
怎么快速定位应用的热点,生成火焰图?
总结一句话:查看任意方法调用情况,传入参数,返回值,反编译源码,热更新等等。
java -jar arthas-boot.jar
输入help,查看支持哪些命令。下面介绍几个关键的命令:sc、watch、
sc 命令
watch命令
sm命令
jad命令
我们以修改上面漏洞的Rocketmq-client客户端Topic合法检查为例子,哪么需要下面几步,命令如下:
使用的命令如下:
java -jar arthas-boot.jar --telnet-port 9996 --http-port -1
jad --source-only org.apache.rocketmq.client.Validators > /tmp/Validators.java
vim /tmp/Validators.java, 注释掉check处代码
sc -d *Validators | grep classLoaderHash
mc -c 2a139a55 /tmp/Validators.java -d /tmp
redefine /tmp/org/apache/rocketmq/client/Validators.class
效果如下:
这里以某套大数据分析工具的漏洞为例子,漏洞如下:
找到漏洞存在的本质:错误的使用预编译方法,将SQL语句拼接了传入prepareStatement函数。
类:java.sql.PreparedStatement 函数:prepareStatement 参数sql
类:java.sql.Statement 函数:executeQuery 参数 sql
对比一下正确和错误的用法如下:
所以,我们使用watch命令关注prepareStatement函数,如果直接把我们的参数拼接进了SQL语句并传入了这个函数,哪么一定是存在SQL注入漏洞的。watch命令如下: watch java.sql.Connection prepareStatement "params[0]" -n 888888
甚至可以用于webshell的取证哟,比如冰蝎工具的。 options unsafe true
其他更多的玩法,更多的技巧,可以看使用文档:https://alibaba.github.io/arthas/index.html
也欢迎大家去发现更多的玩法,然后和我交流带带我。
参考文章:
1. https://alibaba.github.io/arthas/index.html
2. 认识 JavaAgent
https://paper.seebug.org/1099/
3. 探秘 Java 热部署三 (agentmain)
插播一则小广告
**base:**成都 or 深圳
**招聘:**安全攻防、安全研究
**WeChatID:**lc10516