之前审计的过程中,遇到过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的漏洞,所以先放过吧。