探究Dubbo反序列化RCE之新利用-DubboRouge

2020-03-16 约 190 字 预计阅读 1 分钟

声明:本文 【探究Dubbo反序列化RCE之新利用-DubboRouge】 由作者 threedr3am 于 2020-03-16 09:55:17 首发 先知社区 曾经 浏览数 199 次

感谢 threedr3am 的辛苦付出!

0x01 前言

前段时间写了篇关于Dubbo在默认dubbo协议下,使用hessian2序列化方式的利用讲解文章《dubbo源码浅析:默认反序列化利用之hessian2》,我发现网络上并没有存在过讲解这方面利用的文章,其实这么简单,想必很多大佬在这之前已经知道了...

文章发了之后,我觉得应该很多公司都对其Dubbo服务的安全隐患进行了排查,对于一些没有安全审查能力的公司,可能会留下了比较大的安全隐患,故而,我又写了篇关于Hessian2反序列化安全加固的文章《dubbo反序列化问题-Hessian2安全加固和修复》 ,原理也非常之简单,其实就是加入黑名单,相对于原生Java反序列化原理差不多,只不过原生Java反序列化已经有实现了,只要配置系统环境变量java.serialization去配置Filter就好了。

前面,所讲解的都是Dubbo服务端的攻击手法,那么,我们有没有办法去攻击Dubbo客户端呢?既然Dubbo服务端能接收客户端发来的序列化数据进行反序列化,造成RCE,那么对于服务端的恶意序列化数据响应,必然也会造成客户端反序列化RCE吧?

带着这些疑问,我对其源码进行了一番研究,并且进行了实验。我们知道,使用Dubbo的时候,我们一般使用Zookeeper作为注册中心,也可以不使用注册中心,选择直连的方式,参考官方文档

若客户端选择了直连的方式,我们就可以类似Mysql Rouge、Redis Rouge的方式,去部署一个恶意的服务,在客户端连接上来后,返回恶意的序列化数据,造成客户端反序列化RCE。具体实现,后面我会在github开源的项目learn-java-bug放出,在dubbo这个module的com.threedr3am.bug.dubbo.rouge包下,具体使用方式,有两种反序列化攻击hessian2和原生java,具体选用随意,只要在攻击代码中修改一下以下代码即可:

String zookeeperUri = "127.0.0.1:2181";//直连模式下,无需关心这个配置
String rougeHost = "127.0.0.1";//当前恶意服务所在ip
int rougePort = 33336;//当前恶意服务通讯端口

new DNSURL().startRougeServer(zookeeperUri, rougeHost, rougePort, bytes, false);//直连模式下,startRougeServer方法最后一个attackRegister参数必须为false

上面只是直连的攻击手法,个人觉得比较low的,因为你没办法控制客户端的配置,既然都不可控,就谈不上直连攻击了。那么,有没有更容易的利用手法呢?我们能不能直接连接客户端,发送恶意序列化数据?

经过一番源码论证以及试验,也行不通,因为客户端默认也是使用netty,和服务端建立tcp长连接,也就是说,客户端不监听tcp连接,它只会主动建立连接,那么,这里是不是可以考虑tcp的攻击?是不是可以遍历seq,去伪造数据包?我这里不对其进行考虑和研究,当然是因为有更好的利用手法啦!


0x02 dubbo服务集群

为了避免单点故障,现在的应用通常至少会部署在两台服务器上。对于一些负载比较高的服务,会部署更多的服务器。这样,在同一环境下的服务提供者数量会大于1。对于服务消费者来说,同一环境下出现了多个服务提供者。这时会出现一个问题,服务消费者需要决定选择哪个服务提供者进行调用。另外服务调用失败时的处理措施也是需要考虑的,是重试呢,还是抛出异常,亦或是只打印异常等。为了处理这些问题,Dubbo 定义了集群接口 Cluster 以及 Cluster Invoker。集群 Cluster 用途是将多个服务提供者合并为一个 Cluster Invoker,并将这个 Invoker 暴露给服务消费者。这样一来,服务消费者只需通过这个 Invoker 进行远程调用即可,至于具体调用哪个服务提供者,以及调用失败后如何处理等问题,现在都交给集群模块去处理。集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是集群的作用。

上面是抄摘Dubbo官方文档的一番描述,Dubbo是一个具备高可用特性的RPC框架,为了高可用的特性,若单点部署,出现故障就跟高可用毛线关系没有了,所以,一般很多企业都会对其进行生产可用性的调整,无非就是集群部署,部署多个节点,那么,这个时候,若是使用注册中心的方式,就能达到动态的扩容和下线了,因为集群服务的每一台机器,都把自己的一些连接和配置信息放在了Zookeeper(其它的注册中心也一样,类似nacos等等)上了。

既然是通过Zookeeper去注册新的服务,让客户端去发现,进行连接使用。那么,这就是一个利用突破点,若我们把前面所说的,我们的dubbo rouge恶意服务注册到Zookeeper,这样,在客户端进行负载均衡的时候,就会有几率连接到我们的恶意服务,从而使dubbo客户端能接收并反序列化我们的恶意序列化数据,最终RCE,更绝的是,我们可以把其它机器的注册信息删除了,那么客户端就剩下我们的恶意服务可以连接了,一打一个准。


0x03 恶意服务注册

上一小节说了,把我们的恶意服务注册到Zookeeper来,经过一番调试,可以看到,默认情况下,以某service服务的全限定名com.threedr3am.learn.server.boot.DemoService为例,存在Zookeeper的路径为:

/dubbo/com.threedr3am.learn.server.boot.DemoService}/providers/

对于每一个dubbo服务,都会在providers目录下,新建一个children path,一个进行了URLEncode的path,例:

dubbo%3A%2F%2F127.0.0.1%3A20881%2Fcom.threedr3am.learn.server.boot.D
emoService%3Factives%3D5%26anyhost%3Dtrue%26application%3Dservice-pr
ovider%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26gener
ic%3Dfalse%26interface%3Dcom.threedr3am.learn.server.boot.DemoServic
e%26methods%3Dhello%26pid%3D28092%26release%3D2.7.5%26retries%3D3%26
revision%3D1.0%26side%3Dprovider%26time
out%3D3000%26timestamp%3D1582005204823%26version%3D1.0

可以看到,上面的信息,包含了dubbo服务所在的ip和port

既然如此,那么我们只要加入一个同样的path,指向我们的恶意服务即可。但是,我们怎么确定service名称呢?其实,并不需要,我们只要遍历到providers目录,从providers取出已有的一个path,对其进行修改成我们恶意服务的ip和port后添加就行了,具体实现代码就不放了。

并且,更绝的是,我们把其他的注册信息删除了,因此,客户端根据负载均衡,只能选择唯一的,也就是我们的恶意服务进行RPC了,在对其进行了测试之后,效果很好,客户端的RPC请求里面就发过来了,那么我们就可以进行下一步了攻击了。


0x04 dubbo服务治理

上图所示为dubbo的服务治理架构图,在dubbo2.7以后,多了三大feature,其中之一为元数据中心,习惯使用dubbo的人一般都比较了解,当集群部署dubbo的服务,每一个dubbo服务都会把自身信息注册至注册中心,也就是途中,dubbo客户端可访问的注册中心。

随着注册信息的增加,数据量的膨胀,会导致注册中心不断增大网络开销,直接造成了服务地址推送慢等负面影响,因为在2.7版本以前,注册中心存放在大量注册信息无关的信息,因此在2.7以后,为了避免数据量的膨胀导致注册中心不断增大网络开销,新增了元数据中心,用于存储此类与注册无关的信息,其中包含了服务的方法列表以及参数列表等等。

但dubbo开发者,为了降低开发者使用dubbo的难度,还是存在着把部分信息存在了注册中心,具体是何信息呢?我们以zookeeper为例,展开探究。


0x05 控制客户端序列化类型

上一小节,讲述了如何去把恶意服务注册到注册中心,但是,我们在默认情况下,一般都是使用Hessian2的序列化方式,它的可利用的gadget有点少,因此,利用受限还是挺严重的。

若是客户端使用的是原生Java序列化方式,那么,我们的攻击威力,瞬间就大增了,但是,一般情况下,很多Dubbo的使用者,他们都是使用默认缺省配置,也就是Hessian2的序列化方式,凉凉...

但我们回想一下,在使用dubbo的时候,我们是不是只听过服务端配置序列化类型,而没听过使用客户端配置序列化类型?

在回顾我们前面所说的,服务端往注册中心写的信息:

dubbo%3A%2F%2F127.0.0.1%3A20881%2Fcom.threedr3am.learn.server.boot.D
emoService%3Factives%3D5%26anyhost%3Dtrue%26application%3Dservice-pr
ovider%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26gener
ic%3Dfalse%26interface%3Dcom.threedr3am.learn.server.boot.DemoServic
e%26methods%3Dhello%26pid%3D28092%26release%3D2.7.5%26retries%3D3%26
revision%3D1.0%26side%3Dprovider%26time
out%3D3000%26timestamp%3D1582005204823%26version%3D1.0

对其进行decode:

dubbo://127.0.0.1:20881/com.threedr3am.learn.server.boot.DemoService
?actives=5&anyhost=true&application=service-provider&deprecated=fals
e&dubbo=2.0.2&dynamic=true&generic=false&interface=com.threedr3am.le
arn.server.boot.DemoService&methods=hello&pid=28092&release=2.7.5&re
tries=3&revision=1.0&side=provider&timeout=
3000&timestamp=1582005204823&version=1.0

好像并没有发现有什么异处。

在进一步对序列化和反序列化相关代码审计之后,我发现,客户端的序列化方式,居然是根据服务端的配置来选择,那么,我们是不是就对其序列化类型可控了?

在我通过对服务端序列化类型配置为原生java类型后,我发现。

dubbo://127.0.0.1:20881/com.threedr3am.learn.server.boot.DemoService
?actives=5&anyhost=true&application=service-provider&deprecated=fals
e&dubbo=2.0.2&dynamic=true&generic=false&interface=com.threedr3am.le
arn.server.boot.DemoService&methods=hello&pid=28092&release=2.7.5&re
tries=3&revision=1.0&serialization=java&side=provider&timeout=
3000&timestamp=1582005204823&version=1.0

zookeeper的注册信息中,居然多出了一个配置项serialization=java,没错,默认情况下,这个配置是缺省的,缺省情况下,客户端会选择hessian2的序列化方式,若在注册信息中,加入该配置,客户端在读取该注册信息并连接上我们的dubbo服务后,它就选择了原生java的序列化方式了。

因此,我们就能通过追加serialization参数去自由选择客户端的序列化方式了,例如serialization=java,别忘了,根据dubbo协议以及源码的判断,还得把恶意响应包的头部的序列化标识id修改为原生Java的id,具体数字看:org.apache.dubbo.common.serialize.Constants

package org.apache.dubbo.common.serialize;

public interface Constants {
    byte HESSIAN2_SERIALIZATION_ID = 2;
    byte JAVA_SERIALIZATION_ID = 3;
    byte COMPACTED_JAVA_SERIALIZATION_ID = 4;
    byte FASTJSON_SERIALIZATION_ID = 6;
    byte NATIVE_JAVA_SERIALIZATION_ID = 7;
    byte KRYO_SERIALIZATION_ID = 8;
    byte FST_SERIALIZATION_ID = 9;
    byte NATIVE_HESSIAN_SERIALIZATION_ID = 10;
    byte PROTOSTUFF_SERIALIZATION_ID = 12;
    byte AVRO_SERIALIZATION_ID = 11;
    byte GSON_SERIALIZATION_ID = 16;
    byte PROTOBUF_JSON_SERIALIZATION_ID = 21;
}

0x06 发送恶意序列化数据

既然,我们可以注册恶意服务,并且还能控制客户端的反序列化方式,那么,只要注册中心可控,我们就能畅通无阻,而且,这种打法,比打服务端更销魂,打服务端,我们得针对特定ip、port去打,若使用者更换了端口号,我们还得去扫出来,说不定就触发蜜罐了。

而这种方式,当然,注册中心的端口可能也会被定制的修改掉,但是比起dubbo,根据我的个人经验,我有理由相信概率更低。so,现在我们只要等客户端送上门就行了,哈哈,我终于理解 rouge 中文意指 胭脂 的意思了...

脚本编写,我这边以commons-collections:commons-collections:3.2.1的gadget为例。

依赖(这里需要特别注意Zookeeper的版本,后面等开放项目learn-java-bug中的利用demo后,再慢慢加上其它注册中心利用的demo)

参考

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now