之前审计的过程中,遇到过Dubbo这个组件,虽然知道这个组件存在反序列化漏洞,但是关于漏洞的详情和利用一概不知,所以下面对Dubbo的漏洞进行分析。
背景
Dubbo是一款开源的RPC和微服务治理的框架,最早是阿里开发的后来归到了Apache下面,支持多种协议,比如gRPC、Thrift、JsonRPC、Hessian2、REST、RMI、HTTP。
下面是官网对于Dubbo架构的介绍。

主要包含消费者、服务提供者、注册中心、监控中心、容器等等。

provider在启动时会将服务注册到注册中心,consumer在使用时会从注册中心获取到已经注册的服务,再根据consumer中配置的路由决定调用哪个服务,consumer通过RPC协议调用provider的服务并获取返回结果。
CVE-2019-17564
环境搭建
github下载官方提供的https://github.com/apache/dubbo-samples项目,导入simple-http。

直接下载的项目跑不起来,要更改http-provider.xml和http-consumer.xml的内容。
1 | <beans xmlns="http://www.springframework.org/schema/beans" |

由于漏洞影响的版本为2.7.3所以要更改pom.xml的配置,注意是在dependency下面新增一个version标签,<version>2.7.3</version>。

还有坑点,原本的配置是用${}包含配置文件的,但是低版本解析过程并没有解析${}中的内容,所以要进行更改。

更改后zookeeper更改后如下,port和server的参数更改类似。


下面依次启动zookeeper,provider和Consumer即可,调试好后可以添加低版本的CC依赖,可以直观的看到利用结果。
1 | <dependency> |
漏洞分析
在漏洞分析前我们还要再铺垫一些前置知识,provider在导出服务并注册到注册中心zookeeper时,实际上注册的是一个URL地址,但是这个URL并不是java.net.URL而是Dubbo自定义的URL,包含了下面几个属性。
protocol:一般是 dubbo 中的各种协议 如:dubbo thrift http zk
username/password:用户名/密码
host/port:主机/端口
path:接口名称
parameters:参数键值对
下面是URL的一些栗子。
1 | dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000 |
其中访问的path可以在dubbo service的bean配置中通过path属性进行设置,如果没有设置,则默认使用interface的名称当作path。

同样,由于Dubbo在进行RPC调用时支持多种协议,也可以在Bean配置中通过protocol的name属性进行配置,由于CVE-2019-17564是Dubbo以http协议通信时出现的问题,因此我们这里配置也使用http的方式。
1 | <dubbo:protocol name="http" id="http" port="8080" server="tomcat"/> |
通过上面的配置,假如我们要访问DemoService服务,可以通过下面的url访问。
1 | http://127.0.0.1:8080/org.apache.dubbo.samples.http.api.DemoService |
现在我们再回到这个漏洞本身,通过官网的介绍Dubbo在实现http协议时使用的是spring的HttpInvoker来实现的。

我们再来了解HttpInvoker的介绍,其实现是基于标准的Java的序列化和反序列化机制,这里之所以要通过序列化和反序列化来实现,是因为在进行RPC调用时,调用的参数和返回值不一定是基本类型,也有可能是对象,而对象的传递肯定是要基于序列化和反序列,值得一提的是Dubbo在实现RPC调用时,会将我们要调用的方法和参数封装到RemoteInvocation对象中,服务端再接收到请求后,会将请求的内容反序列化为RemoteInvocation对象再去做其他操作,当我们了解了这些以后,即使不去看实现也能想到,在服务端provider接收到请求并进行反序列化时可能出现反序列化漏洞。

客户端
之前在分析Dubbo源码时已经分析了Http请求的处理过程,这里就不仔细分析了,把重点的代码说明一下即可。在org.springframework.remoting.httpinvoker.AbstractHttpInvokerRequestExecutor#executeRequest中,会对RemoteInvocation对象进行序列化。

在RemoteInvocation对象中封装了调用的方法名、请求参数类型、参数值等信息。

下面通过org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor#doExecuteRequest发送请求,prepareConnection主要完成请求发送前的一些设置。

在org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor#prepareConnection中可以看到通过POST发送请求。

writeRequestBody中将序列化的RemoteInvocation对象作为post请求的内容发送给服务端。

服务端
当请求过来时,交给DispatcherServlet会将请求交给org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#handleRequest处理,这个方法会通过readRemoteInvocation将request中的内容转换为RemoteInvocation对象。

获取request对象中的请求内容,在doReadRemoteInvocation中通过readObject反序列化,如果反序列化的对象不是RemoteInvocation对象则抛出异常。


通过上面的分析,我们可以看出Dubbo Provider服务端在执行Consumer过来的请求时直接从POST中获取对象并进行反序列化,在反序列化之前并没有做其他安全的判断,所以我们以POST发送任意一个对象到Provider都会被反序列化,如果我们传入一个特殊构造的对象,Provider反序列化后则有可能造成命令执行或其他操作。
是不是一定要知道要调用的Provider提供服务的method和参数才能触发反序列化?
这里需要了解服务导出的操作,我是以Tomcat做为服务端的,可以看到在构造Tomcat服务器时,添加了ServletMapping为*,也就是所有的请求都会被我们注册的handler处理。

但是在org.apache.dubbo.rpc.protocol.http.HttpProtocol.InternalHandler#handle中会做如下处理。

首先获取请求uri并判断是否能找到对应的Exporter处理,如果找不到则会导致异常,SkeletonMap中保存的路径如果没有在provider的service元素中配置path属性,则默认使用interface的内容。


所以并不是访问服务端的任意接口都能触发漏洞的。
漏洞复现
漏洞复现就比较简单了,首先生成EXP
1 | java -jar e:\tools\ysoserial.jar CommonsCollections4 "calc.exe" > c:\users\admin\desktop\payload.ser |
发送序列化的数据即可利用成功。 
漏洞修复
在Dubbo 2.7.4或 2.6.8 or 2.6.9修复了这个漏洞,我们看下漏洞的修复,首先看下服务导出的变化,在org.apache.dubbo.rpc.protocol.http.HttpProtocol#doExport中,不再使用HttpServiceExporter而是使用JsonRpcServer作为Exporter。

当我们请求服务时,也不再使用HttpServiceExporter处理请求而是使用JsonRpcBasicServer处理请求。

handleNode中根据类型的不同做不同的处理。

从ObjectNode中获取参数和调用方法等信息。

得到要调用方法的method对象和参数值和实现类的代理对象后调用 invoke方法。

将参数类型做转换后通过反射完成调用。

这个过程中并没有涉及到JAVA本身的反序列化,不过还是涉及到JSON的反序列化操作,使用JACKSON进行反序列化,不过我并不熟悉JACKSON的漏洞,所以先放过吧。