沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Java反序列化流程分析及resolveClass

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 总结


总结一下,就是如下图所示,非常简单:
image-20220918212535766

1.readObject进入readObject0,根据字节选择对应的方法,比如类就会进入readOrdinaryObject

2.readOrdinaryObject中先获取类描述符,进入readClassDesc,根据字节选择对应的类描述符获取方法,其中会调用resolveClass反射实例化我们的目标类

3.readOrdinaryObject再进入readSerialData,根据刚刚的类描述符去填充数据,其中如果目标类重写了readObject则调用,否则就使用默认的填充方法。

参考


https://blog.kaibro.tw/2020/02/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BreadObject%E5%88%86%E6%9E%90/

https://www.cnpanda.net/sec/928.html