Hessian 反序列化浅析

文章发布时间:

最后更新时间:

Hessian 反序列化学习

  • 简单了解下 Hessian

    Hessian 是一种动态类型、二进制序列化和 Web 服务协议,旨在用于面向对象的传输。

    「Hessian」的简单使用

    1. 基于 servlet:

      • 配置 server 端

        只需要继承 HessianServlet 即可

        1
        2
        3
        4
        5
        6
        public class HelloServlet extends HessianServlet implements Greeting {
        @Override
        public String sayHello(HashMap o) {
        return "hello" + o.toString();
        }
        }

        web.xml 如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        <servlet>
        <servlet-name>helloServlet</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
        </servlet>

        <servlet-mapping>
        <servlet-name>helloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
        </servlet-mapping>
      • 配置客户端:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        public static void main(String[] args) throws Exception {
        String url = "http://localhost:8080/hello";

        HessianProxyFactory factory = new HessianProxyFactory();
        Greeting greeting = (Greeting) factory.create(Greeting.class, url);

        HashMap o = new HashMap<>();
        o.put("a", "a");

        System.out.println("Hessian Call: " + greeting.sayHello(o));

        }

        image-20230324200000065

    2. 基于 spring 项目

      image-20230325104642710

  • 源码分析:

    • Servlet 部分

      HessianServlet 继承自 HttpServlet 实现了 init 和 service 等方法

      init 部分对成员变量进行的初始化操作

      image-20230325105730932

      其中在进行类加载时采用的自定义 loadClass 方法

      image-20230325105413649

      这里使用自定义类加载机制的原因:

      • 不同环境下可能使用自定义类加载器重新加载类,对原来的代码进行魔改,这里可以确保拿到原本的代码。
      • 线程中一般默认是 AppClassLoader,是加载用户代码的类加载器,通常可以很快找到用户的类。

      service 部分处理逻辑:

      image-20230325110023231

      1. 请求仅支持 POST 方法,否则返回 500
      2. 获取请求中的 id 或 ejbid 参数并赋值给 objectId
      3. 设置响应体的 contentType
      4. invoke 调用处理逻辑

      跟进看 invoke,会根据参数 objectId 决定调用哪个

      image-20230325110221025

      再看 HessianSkeleton 这个类,其继承于 AbstractSkeleton ,负责对 Hessian 提供的服务进行封装。先看下 AbstractSkeleton 所作工作:抽离出 apiClass 中的所有方法,针对每个方法依次将 <方法名, 方法> 以及 <方法名__<参数长度>, 方法> 放入 _methodMap 当中

      image-20230325110633707

      而对于 HessianSkeleton,初始化时将服务对象封装在 _service 字段

      image-20230325110912426

      然后再看 invoke 方法逻辑,这里会先读取协议头部字段,根据头部值来决定使用哪种输入输出流类型,之后调用 invoke(_service, in, out)

      image-20230325111328423

      跟进后,可以看到首先读取方法名和参数长度,构建之前的自定义签名并查找该方法。之后利用反序列化参数值,反射调用 servie 对象的方法并将结果写回返回流

      image-20230325111949534

    • Spring 部分

      处理逻辑基本上和 Servlet 的是一样的,另外这里也重写了类加载器

      image-20230325112712517

    • 序列化与反序列化流程

      「序列化」:Hessian2Output#writeObject(),这里会根据对象类型来调用具体的 serializer 的 writeObject 方法。对于自定义类默认是 UnsafeSerializer

      image-20230325113053304

      我们查看 UnsafeSerializer#writeObject() ,可以看到先调用了 writeObjectBegin()

      image-20230325113430375

      这里对于 1.0 和 2.0 版本的 Hessian 处理是不同的

      image-20230325113646892

      Hessian 2.0 中将会调用 writeDefinition20Hessian2Output#writeObjectBegin 方法写入自定义数据,就不再将其标记为 Map 类型

      「反序列化」:Hessian2Input#readObject(),会根据读取的标识位调用不同实现类来处理

      image-20230325114303396

      与序列化时一样,Hessian 2.0 以前对于自定义类将以 map 类型标记,因此在反序列化时也会按照 map 类型来读取

      image-20230325114532585

      实际调用的是 MapDeserializer#readMap(),会循环遍历反序列化内容,并将结果 put 进创建的 map 中

      image-20230325114717350

      对于 Hessian 2.0 则用 UnsafeDeserializer#readObject 来处理。instantiate 方法直接用 Unsafe 类静态方法创建的实例;之后反序列化读取 field 值

      image-20230325114945563

  • 漏洞

    之前提到反序列化时对于 map 标记类型的序列化数据调用的是 MapDeserializer#readMap() 根据指定的 _type 选择 HashMap 还是 SortedMap (接口实现用的是 TreeMap

    之后遍历反序列化 key 和 value 并放入 map,对比一下这两种类的 put 操作:

    1. HashMap put 时会触发 key 的 hashCode() 方法

      image-20230325150645916

    2. TreeMap put 时会触发 key 的 compareTo 方法

      image-20230325150745378

      其次还有一个限制在于对于 transient 和 static 修饰的成员变量,是不会参与到反序列化流程中的,具体原因可见 UnsafeSerializer#introspect() ,在遇到这两个字段的变量时会直接忽略

      image-20230325151041483

    总结限制如下:

    • kick-off chain 起始方法只能为 hashCode/equals/compareTo 方法;
    • 利用链中调用的成员变量不能为 transient 修饰;
    • 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。
  • Gadget

    0x1. Rome

    该条利用链的核心在于 ToStringBean#toString() 方法,可以任意调用无参 getter 方法,那么就可以配合 JdbcRowSetImpl#getDatabaseMetaData() 实现 JNDI 注入

    image-20230325155444971

    外层封装利用的是 EqualsBean 和 HashMap ,触发点在 HashMap put 操作上

    image-20230325155803374

    如果不出网的话这个 gadget 就无了,并且 TemplatesImpl 也是行不通的,因为这里的 _tfactory 字段被 transient 所修饰

    image-20230325162035549

    因此接下来的利用思路就是要么找二次反序列化,要么找不被 transient 修饰的且可直接利用(代码执行/命令执行)的函数

  • 参考链接

    [1]https://su18.org/post/hessian/

    [2]https://y4tacker.github.io/2022/03/21/year/2022/3/2022%E8%99%8E%E7%AC%A6CTF-Java%E9%83%A8%E5%88%86/#%E6%AD%A3%E6%96%87