作者:Longofo@知道创宇404实验室
时间:2020年2月20日
原文地址:https://paper.seebug.org/1131/#_2
前不久有一个关于Apache Dubbo Http反序列化的漏洞,本来是一个正常功能(通过正常调用抓包即可验证确实是正常功能而不是非预期的Post),通过Post传输序列化数据进行远程调用,但是如果Post传递恶意的序列化数据就能进行恶意利用。Apache Dubbo还支持很多协议,例如Dubbo(Dubbo Hessian2)、Hessian(包括Hessian与Hessian2,这里的Hessian2与Dubbo Hessian2不是同一个)、Rmi、Http等。Apache Dubbo是远程调用框架,既然Http方式的远程调用传输了序列化的数据,那么其他协议也可能存在类似问题,例如Rmi、Hessian等。@pyn3rd师傅之前在twiter发了关于Apache Dubbo Hessian协议的反序列化利用,Apache Dubbo Hessian反序列化问题之前也被提到过,这篇文章里面讲到了Apache Dubbo Hessian存在反序列化被利用的问题,类似的还有Apache Dubbo Rmi反序列化问题。之前也没比较完整的去分析过一个反序列化组件处理流程,刚好趁这个机会看看Hessian序列化、反序列化过程,以及marshalsec工具中对于Hessian的几条利用链。
关于序列化/反序列化机制
序列化/反序列化机制(或者可以叫编组/解组机制,编组/解组比序列化/反序列化含义要广),参考marshalsec.pdf(原文链接地址可下载),可以将序列化/反序列化机制分大体分为两类:
基于Bean属性访问机制
基于Field机制
基于Bean属性访问机制
SnakeYAML
jYAML
YamlBeans
Apache Flex BlazeDS
Red5 IO AMF
Jackson
Castor
Java XMLDecoder
…
它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)
和setter(xxx)
访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。
基于Field机制
基于Field机制是通过特殊的native(native方法不是java代码实现的,所以不会像Bean机制那样调用getter、setter等更多的java方法)方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,不是通过getter、setter方式对属性赋值(下面某些处理器如果进行了特殊指定或配置也可支持Bean机制方式)。在ysoserial中的payload是基于原生Java Serialization,marshalsec支持多种,包括上面列出的和下面列出的。
Java Serialization
Kryo
Hessian
json-io
XStream
…
就对象进行的方法调用而言,基于字段的机制通常通常不构成攻击面。另外,许多集合、Map等类型无法使用它们运行时表示形式进行传输/存储(例如Map,在运行时存储是通过计算了对象的hashcode等信息,但是存储时是没有保存这些信息的),这意味着所有基于字段的编组器都会为某些类型捆绑定制转换器(例如Hessian中有专门的MapSerializer转换器)。这些转换器或其各自的目标类型通常必须调用攻击者提供的对象上的方法,例如Hessian中如果是反序列化map类型,会调用MapDeserializer处理map,期间map的put方法被调用,map的put方法又会计算被恢复对象的hash造成hashcode调用(这里对hashcode方法的调用就是前面说的必须调用攻击者提供的对象上的方法),根据实际情况,可能hashcode方法中还会触发后续的其他方法调用。
Hessian简介
Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。本文主要讲解Hessian的序列化/反序列化。
下面做个简单测试下Hessian Serialization与Java Serialization:
//Student.java
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private transient String gender;
public int getId() {
System.out.println("Student getId call");
return id;
}
public void setId(int id) {
System.out.println("Student setId call");
this.id = id;
}
public String getName() {
System.out.println("Student getName call");
return name;
}
public void setName(String name) {
System.out.println("Student setName call");
this.name = name;
}
public String getGender() {
System.out.println("Student getGender call");
return gender;
}
public void setGender(String gender) {
System.out.println("Student setGender call");
this.gender = gender;
}
public Student() {
System.out.println("Student default constractor call");
}
public Student(int id, String name, String gender) {
this.id = id;
this.name = name;
this.gender = gender;
}
@Override
public String toString() {
return "Student(id=" + id + ",name=" + name + ",gender=" + gender + ")";
}
}
//HJSerializationTest.java
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class HJSerializationTest {
public static <T> byte[] hserialize(T t) {
byte[] data = null;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(os);
output.writeObject(t);
data = os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public static <T> T hdeserialize(byte[] data) {
if (data == null) {
return null;
}
Object result = null;
try {
ByteArrayInputStream is = new ByteArrayInputStream(data);
HessianInput input = new HessianInput(is);
result = input.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return (T) result;
}
public static <T> byte[] jdkSerialize(T t) {
byte[] data = null;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(os);
output.writeObject(t);
output.flush();
output.close();
data = os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public static <T> T jdkDeserialize(byte[] data) {
if (data == null) {
return null;
}
Object result = null;
try {
ByteArrayInputStream is = new ByteArrayInputStream(data);
ObjectInputStream input = new ObjectInputStream(is);
result = input.readObject();
} catch (Except