0x00 前言
好不容易遇到一位专业的面试师傅,问的问题都是自己接触过的东西,但是不知道为什么,在面试的时候,脑子就一片空白,什么也不记得了……感觉没有把握好机会。不过这也说明自己确实还是不太熟悉吧,看来还是要经常复习和思考,避免”学而不思则罔“的尴尬,所以本文再来理一下反序列化的流程,并从原理上理解为什么重写resolveClass可以用来防御反序列化。
0x01 环境准备
测试代码
编写如下的类,其重写了我们的readObject:
public class Foobar implements Serializable {
public String name;
public String age;
public Foobar(){
this.name = "chenlvtang";
this.age = "22";
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
java.lang.Runtime.getRuntime().exec("calc");
}
}
编写序列化和反序列化的主函数:
public class Main {
public static void main(String[] args) throws Exception{
Foobar foobar = new Foobar();
// 开始序列化
FileOutputStream fileOutputStream = new FileOutputStream("chenlvtang.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(foobar);
// 开始反序列化
FileInputStream fileInputStream = new FileInputStream("chenlvtang.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
}
}
在readObject处打下断点,然后开始调试。
0x02 调试分析
ObjectInputStream#readObject
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
// 注意这里
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
// 略
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
}
enableOverride来自ObjectInputStream构造方法,当调用其无参构造函数时会设定为true,我们这里因为传了fileInputStream,所以为flase,并不会进入。可以看到Obj的获取来自readObject0,所以继续跟进。
ObjectInputStream#readObject0
private Object readObject0(boolean unshared) throws IOException {
// 略去对读取模式的检查
// 读取一个字节
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
// 略
case TC_ENDBLOCKDATA:
// 略
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
readObject0中先读取一个字节,这个字节是aced(序列化Stream标识)0005(版本号)
之后的第一个字节,这里为0x73
,即十进制的115,之后便会根据这个值进入对应的分支:TC_OBJECT,且在这个分支之中会进入readOrdinaryObject之中。
ObjectInputStream#readOrdinaryObject
private Object readOrdinaryObject(boolean unshared) throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
// 略
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
//略
return obj;
}
在其中可以看到,obj的实例化来自于desc,而desc来自readClassDesc
ObjectInputStream#readClassDesc
private ObjectStreamClass readClassDesc(boolean unshared)
throws IOException
{
byte tc = bin.peekByte();
ObjectStreamClass descriptor;
switch (tc) {
case TC_NULL:
descriptor = (ObjectStreamClass) readNull();
break;
case TC_REFERENCE:
descriptor = (ObjectStreamClass) readHandle(unshared);
break;
case TC_PROXYCLASSDESC:
descriptor = readProxyDesc(unshared);
break;
case TC_CLASSDESC:
descriptor = readNonProxyDesc(unshared);
break;
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
if (descriptor != null) {
validateDescriptor(descriptor);
}
return descriptor;
}
在readObjectDesc中,会再次更加类型进入到各分支中。我们这里是一个类,所以会进入TC_CLASSDESC,使用readNonProxyDesc获得类的描述信息(字段值之类的)。
ObjectInputStream#readNonProxyDesc
private ObjectStreamClass readNonProxyDesc(boolean unshared)throws IOException
{
// 略
ObjectStreamClass readDesc = null;
try {
// 获取属性的值
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}
Class<?> cl = null;
ClassNotFoundException resolveEx = null;
bin.setBlockDataMode(true);
final boolean checksRequired = isCustomSubclass();
try {
// 解析我们的类
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex;
}
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
handles.finish(descHandle);
passHandle = descHandle;
return desc;
先是使用readClassDescriptor()获得了我们的字段信息,然后使用resolveClass去解析。最后使用initNonProxy把刚刚的字段信息,装载成类描述符,然后返回。
ObjectInputStream#resolveClass
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
// 根据我们的类描述,反射调用
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
可以看到,在resolveClass中利用反射去调用我们的类,这也是许多反序列化选择在此处加上黑名单的原因。
ObjectInputStream#readSerialData
回到readOrdinaryObject,刚刚我们说到会先获得类描述符,之后经过checkDeserialize()检查是否可反序列化后,就会进入readSerialData(如果是接口的话就会进入readExternalData):
// ObjectInputStream#readOrdinaryObject
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
其代码如下:
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
} else if (slotDesc.hasReadObjectMethod()) { // 判断是否有重写readObject
// 略
bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this); // 调用重写的readObject
} catch (ClassNotFoundException ex) {
// 略
} finally {
// 略
} else {
defaultReadFields(obj, slotDesc); // 这里
}
if (slotDesc.hasWriteObjectData()) {
skipCustomData();
} else {
bin.setBlockDataMode(false);
}
} else {
if (obj != null &&
slotDesc.hasReadObjectNoDataMethod() &&
handles.lookupException(passHandle) == null)
{
slotDesc.invokeReadObjectNoData(obj);
}
}
}
}
会判断是否有重写的readObject,如果有就调用invokeReadObject,其内部反射调用了我们重写的readObject(这时候就触发了我们实验代码中的RCE),没有就调用defaultReadFields进行数据填充(重写readObject中的in.defaultReadObject其实里面也是调用defaultReadFields)。经过这个函数反序列化基本结束,之后readOrdinaryObject返回Obj,readObject中再调用 vlist.doCallbacks()处理回调,结束反序列化流程,返回反序列化成功的Object。
0x03 总结
总结一下,就是如下图所示,非常简单:
1.readObject进入readObject0,根据字节选择对应的方法,比如类就会进入readOrdinaryObject
2.readOrdinaryObject中先获取类描述符,进入readClassDesc,根据字节选择对应的类描述符获取方法,其中会调用resolveClass反射实例化我们的目标类
3.readOrdinaryObject再进入readSerialData,根据刚刚的类描述符去填充数据,其中如果目标类重写了readObject则调用,否则就使用默认的填充方法。