GadgetInspector改造中反序列化三个source点的原理分析
前言
在改造自动化Gadget挖掘工具GadgetInspector的过程中,针对反序列化漏洞的source点收集时,发现存在三个可能的触发点:readObject、readResolve和readExternal。本文将对这三个方法的调用原理进行详细分析。
实验环境
我们创建了三个测试类来演示这三个source点的调用机制:
Test1类
实现了Serializable接口,并定义了readObject和readResolve方法:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Test1 implements Serializable {
private Object readResolve() {
System.out.println("readResolve.....");
return new Test1();
}
private void readObject(ObjectInputStream inputStream)
throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
System.out.println("readObject.....");
}
}
Test2类
实现了Externalizable接口,并重写了writeExternal和readExternal方法:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Test2 implements Externalizable {
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("writeExternal.....");
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
System.out.println("readExternal....");
}
}
Test类
执行序列化和反序列化操作的主类:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Test {
public static void main(String[] args) throws Exception {
Test1 test1 = new Test1();
Test2 test2 = new Test2();
// 序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(test1);
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
System.gc();
}
}
原理分析
readObject调用机制
-
反序列化入口:
ObjectInputStream#readObject方法是反序列化的主要入口,它会调用readObject0方法执行核心反序列化逻辑。 -
读取对象类型:
readObject0方法通过tc值(从BlockDataInputStream获取的字节)判断对象类型。对于普通对象,tc值为TC_OBJECT(十进制115)。 -
处理普通对象:当
tc值为TC_OBJECT时,调用readOrdinaryObject方法处理普通对象的反序列化。 -
读取类描述符:
readOrdinaryObject调用readClassDesc方法读取类描述符,tc值为TC_CLASSDESC(十进制114)。 -
处理非代理类描述符:
readNonProxyDesc方法读取并返回非动态代理类的类描述符:- 从流中读取类描述
- 调用
resolveClass方法(内部使用Class.forName)加载对应的类 - 将得到的类传递给
desc对象
-
实例化对象:回到
readOrdinaryObject方法,实例化得到obj对象。 -
判断接口类型:通过
isExternalizable方法判断是否实现了Externalizable接口:- 如果未实现(如
Test1),进入else子句调用readSerialData方法
- 如果未实现(如
-
调用readObject方法:在
readSerialData方法中:- 通过
hasReadObjectMethod检查类中是否存在readObject方法 - 如果存在,通过反射调用该方法
- 通过
调用栈:
readObject:14, Test1 (pers.test_01)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
readResolve调用机制
-
在readOrdinaryObject中:调用完
readSerialData方法后,会继续调用hasReadResolveMethod检查序列化类是否存在readResolve方法。 -
反射调用:如果存在
readResolve方法,将通过反射调用该方法。
readExternal调用机制
-
前提条件:目标类必须实现
Externalizable接口(如Test2)。 -
接口判断:在
readOrdinaryObject方法中,isExternalizable方法返回true。 -
调用readExternalData:与
readObject不同,这里调用readExternalData方法处理序列化数据。 -
直接调用readExternal:如果目标类不为空,直接调用其
readExternal方法读取外部化数据。
总结
-
三个source点:
readObject:实现Serializable接口并定义该方法readResolve:实现Serializable接口并定义该方法readExternal:必须实现Externalizable接口
-
自动化工具实现:可以通过模糊匹配方式,筛选实现了
Serializable接口且包含这三个方法之一的类作为Gadget链的source点。 -
调用顺序:在反序列化过程中,这些方法的调用顺序和条件各不相同,理解其底层机制有助于准确识别潜在的漏洞触发点。
通过这种详细分析,我们可以更准确地识别和利用Java反序列化漏洞中的各种source点,为自动化Gadget挖掘工具提供更全面的检测能力。