Java反序列化过程深究

0x01 概述

互联网上大家针对Java反序列化的讨论有很多了,但是这里我还是想聊聊,这里仅仅记录一下自己的学习笔记,之前我在Java 反序列化深究中讨论了为什么,通过重写 readObject 方法会导致反序列化的问题,当然后续整个过程我们也没继续,当然现在继续回来讨论这个事情。

0x02 反序列化过程

这里需要配合我们的反序列化字节流里面的数据来看。

image-20191120103607464.png

而下面是整个过程的调用栈,我们一点点来看。

exec:347, Runtime (java.lang)
readObject:11, ObjectCalc
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1017, ObjectStreamClass (java.io)
readSerialData:1896, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:11, unSerializableCalc

ObjectInputStream#readObject0 ,会根据 tcbyte 值进入 switch 中,选择相关的 case 进行下一步操作。回到之前反序列化字节流中,我们可以看到这里的 TC_OBJECT0x73,所以这里会进入 caseTC_OBJECTreadOrdinaryObject 的进行字节流的处理。

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 10 - 0x00 0a
        Value - ObjectCalc - 0x4f626a65637443616c63
      serialVersionUID - 0x33 73 1c 20 ac f0 18 3b
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      ObjectCalc
        values

跟进 readOrdinaryObject ,在 readOrdinaryObject 中会根据字节流中的下一个关键字 TC_CLASSDESC 进行相关处理,这个 TC_CLASSDESC 是一个 类描述符标识 ,我们可以看到 TC_CLASSDESC 中的 classname 的value正是我们前面测试代码中的 ObjectCalc 这个类名字。

    TC_CLASSDESC - 0x72
      className
        Length - 10 - 0x00 0a
        Value - ObjectCalc - 0x4f626a65637443616c63

而处理上述这些东西的方法自然是 readClassDesc ,这里画个重点,下面反序列化的一些修复方法其实和这个里面的一些有关系,当然这里面还有个 TC_PROXYCLASSDESC 引起了我的注意,这个 TC_PROXYCLASSDESC代理类描述符 ,看到代理类这三个字相信熟悉反序列化的朋友们可能不会陌生,当然不太熟悉的朋友可以了解一下 ysoserial 这个项目,这个项目中大量使用了动态代理方式,这种方式反序列化利用本次不会深入探讨,所以话说回来。

//ObjectInputStream#readOrdinaryObject
private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();
        
//ObjectInputStream#readClassDesc
private ObjectStreamClass readClassDesc(boolean unshared)
        throws IOException
    {
        byte tc = bin.peekByte();
        switch (tc) {
            case TC_NULL:
                return (ObjectStreamClass) readNull();

            case TC_REFERENCE:
                return (ObjectStreamClass) readHandle(unshared);

            case TC_PROXYCLASSDESC:
                return readProxyDesc(unshared);

            case TC_CLASSDESC:
                return readNonProxyDesc(unshared);

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    }

这里我们分别跟进一下 readProxyDescreadNonProxyDesc 方法,首先跟进 readNonProxyDesc 方法,我删除部分代码,可以看看下面代码中的 resolveClassresolveClass 处理的 readDesc 正是我们要利用的一个类 OjectClac

    private ObjectStreamClass readNonProxyDesc(boolean unshared)
        throws IOException
...
        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);
            }
        } 

image-20191120110136619.png

而继续跟进 resolveClass 方法,可以看到通过 Class.forName 方法反射调用我们的利用类 ObjcetCalc

    protected Class<?> resolveClass(ObjectStreamClass desc)
        throws IOException, ClassNotFoundException
    {
        String name = desc.getName();
        try {
            return Class.forName(name, false, latestUserDefinedLoader());
        } 

再回头看看 readProxyDesc 这个类,其实 readProxyDescreadNonProxyDesc 在写法上会发现很像,唯一的区别就是在 resolveProxyClass 这个类上。

    private ObjectStreamClass readProxyDesc(boolean unshared)
        throws IOException
...
        ObjectStreamClass desc = new ObjectStreamClass();
...
        try {
            if ((cl = resolveProxyClass(ifaces)) == null) {
                resolveEx = new ClassNotFoundException("null class");
            } 

跟进 resolveProxyClass 中,这个方法最后会调用 Proxy.getProxyClass 来处理,比如获取代理类这些操作。

    protected Class<?> resolveProxyClass(String[] interfaces)
        throws IOException, ClassNotFoundException
    {
...
        try {
            return Proxy.getProxyClass(
                hasNonPublicInterface ? nonPublicLoader : latestLoader,
                classObjs);

当然经过 readProxyDesc 或者 readNonProxyDesc 处理之后实际上完成了类的实例化,也就说经过 readClassDesc 处理之后,完成了类的实例化,代码继续向下处理,标记了一些注释,这里自然是进入到 readSerialData 中。

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
        ObjectStreamClass desc = readClassDesc(false);   //实例化对象
        desc.checkDeserialize();     //判断对应类是否反序列化

        Class<?> cl = desc.forClass();     //获取类对象,这里是ObjectCalc
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;  //判断是否存在构造器,如果,然后获取类对象实例化,所以这里是实例化的ObjectCalc对象。
        } 
...
        if (desc.isExternalizable()) {       //如果序列化的接口是Externalizable类型,就进入readExternalData,否则进入readSerialData
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);   
        }

Externalizable类型的反序列化类型,可以通过writeExternal()和readExternal()方法指定一个类的部分数据进行序列化与反序列化。
Serializable接口也可以实现类似的机制:将不想要序列化的部分添加一个关键字:transient(临时的)。它声明的变量实行序列化操作的时候不会写入到序列化文件中去。

readSerialData 中有一个 slotDesc.hasReadObjectMethod 判断,而我们在《Java 反序列化深究》讨论过就是它判断是否反序列化,跟进 hasReadObjectMethod 实际上是判断 readObjectMethod 是否为null,并且结果是 boolean 型。

    boolean hasReadObjectMethod() {
        return (readObjectMethod != null);
    }

这个 readObjectMethod 如何来的,实际上需要追溯到前面 readClassDescreadNonProxyDesc 中,实际上,这两个方法实例化类之后,都有一个 desc.initNonProxy 构造方法来处理结果。

//ObjectInputStream#readNonProxyDesc
try {
            if ((cl = resolveClass(readDesc)) == null) {
                resolveEx = new ClassNotFoundException("null class");
            } else if (checksRequired) {
                ReflectUtil.checkPackageAccess(cl);
            }
        } catch (ClassNotFoundException ex) {
            resolveEx = ex;
        }
        skipCustomData();

        desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));

//ObjectInputStream#readClassDesc
        try {
            if ((cl = resolveProxyClass(ifaces)) == null) {
                resolveEx = new ClassNotFoundException("null class");
            } else if (!Proxy.isProxyClass(cl)) {
                throw new InvalidClassException("Not a proxy");
            } else {
                // ReflectUtil.checkProxyPackageAccess makes a test
                // equivalent to isCustomSubclass so there's no need
                // to condition this call to isCustomSubclass == true here.
                ReflectUtil.checkProxyPackageAccess(
                        getClass().getClassLoader(),
                        cl.getInterfaces());
            }
        } catch (ClassNotFoundException ex) {
            resolveEx = ex;
        }
        skipCustomData();

        desc.initProxy(cl, resolveEx, readClassDesc(false));

跟进 initNonProxy 构造方法,实际上这两个方法都会调用 lookup 方法处理 cl ,而 cl 实际上就是我们实例化的那个类,在这个例子中是 ObjectCalc 类。

void initNonProxy(ObjectStreamClass model,
                      Class<?> cl,
                      ClassNotFoundException resolveEx,
                      ObjectStreamClass superDesc)
        throws InvalidClassException
    {
        this.cl = cl;
        if (cl != null) {
            localDesc = lookup(cl, true);

void initProxy(Class<?> cl,
                   ClassNotFoundException resolveEx,
                   ObjectStreamClass superDesc)
        throws InvalidClassException
    {
        this.cl = cl;

        if (cl != null) {
            localDesc = lookup(cl, true);

跟进 lookup 在实例化 ObjectStreamClass ,处理了 cl 对象,而这个 cl 在我们这里面就是 ObjectCalc

 static ObjectStreamClass lookup(Class<?> cl, boolean all) {
        if (!(all || Serializable.class.isAssignableFrom(cl))) {
            return null;
        }
  ...
        if (entry == null) {
            try {
                entry = new ObjectStreamClass(cl);
            } catch (Throwable th) {
                entry = th;
            }

跟进 ObjectStreamClass ,可以看到而这个 cl 在我们这里面就是 ObjectCalc ,而这里会有个判断是否 externalizable ,这个属性我们前面说过了。

    private ObjectStreamClass(final Class<?> cl) {
        this.cl = cl;
        name = cl.getName();
        isProxy = Proxy.isProxyClass(cl);
        isEnum = Enum.class.isAssignableFrom(cl);
        serializable = Serializable.class.isAssignableFrom(cl);
        externalizable = Externalizable.class.isAssignableFrom(cl);
...

        if (serializable) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
...
                    if (externalizable) {    //判断接口是不是
                        cons = getExternalizableConstructor(cl);
                    } else {
                        cons = getSerializableConstructor(cl);
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",  //不是获取cl对象writeObject的私有属性
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",     //不是获取cl对象readObject的私有属性
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);

跟进 getPrivateMethod 方法,可以看到他会对一些属性进行判断。

    private static Method getPrivateMethod(Class<?> cl, String name,
                                           Class<?>[] argTypes,
                                           Class<?> returnType)
    {
        try {
            Method meth = cl.getDeclaredMethod(name, argTypes);
            meth.setAccessible(true);
            int mods = meth.getModifiers();
            return ((meth.getReturnType() == returnType) &&
                    ((mods & Modifier.STATIC) == 0) &&
                    ((mods & Modifier.PRIVATE) != 0)) ? meth : null;
        } catch (NoSuchMethodException ex) {
            return null;
        }
    }

比如拿下面这个例子为例子:

readObjectMethod = getPrivateMethod(cl, "readObject",new Class<?>[] { ObjectInputStream.class },
  • 方法名为readObject
  • 返回类型为void
  • 传入参数为一个ObjectInputStream.class类型参数
  • 修饰符不能包含static
  • 修饰符必须包含private

所以说满足这种情况下才会进入反序列化的下一步核心 slotDesc.invokeReadObject 中,否则会进入 defaultReadFields 进行处理。

    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 &&
                    slotDesc.hasReadObjectMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    SerialCallbackContext oldContext = curContext;

                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
...
                } else {
                    defaultReadFields(obj, slotDesc);
                }

跟进 invokeReadObject ,最后可以看到调用 readObjectMethod.invoke 实际上再往下走就是一些反射方法了。

    void invokeReadObject(Object obj, ObjectInputStream in)
        throws ClassNotFoundException, IOException,
               UnsupportedOperationException
    {
        if (readObjectMethod != null) {
            try {
                readObjectMethod.invoke(obj, new Object[]{ in });
            } 
    }

再回到 ObjectInputStream#readSerialData 中,我们讨论了 slots[i].hasDatatrue 的情况下,实际上当这个 iffalse 的时候来到的是 slotDesc.invokeReadObjectNoData

    private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
						if (slots[i].hasData) {
                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } 
                if (slotDesc.hasWriteObjectData()) {
                    skipCustomData();
                } else {
                    bin.setBlockDataMode(false);
                }
            } else {
                if (obj != null &&
                    slotDesc.hasReadObjectNoDataMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    slotDesc.invokeReadObjectNoData(obj);
                }

而这个判断是实际是根据序列化的时候是不是重写了 readObjectNoData 来进行反序列化。

readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);

再回到我们前面讨论 readOrdinaryObject ,我们讨论了序列化接口不是 Externalizable 类型,如果是的话自然会进入 readExternalData 中。

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
        ObjectStreamClass desc = readClassDesc(false);   //实例化对象
        desc.checkDeserialize();     //判断对应类是否反序列化

        Class<?> cl = desc.forClass();     //获取类对象,这里是ObjectCalc
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;  //判断是否存在构造器,如果,然后获取类对象实例化,所以这里是实例化的ObjectCalc对象。
        } 
...
        if (desc.isExternalizable()) {       //如果序列化的接口是类型,就进入readExternalData,否则进入readSerialData
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);   
        }

跟进 readExternalData 方法,这个方法调用了 obj.readExternal 进行处理,也就是说构造 payload 的时候要达到这个触发点,需要用 writeExternal()和readExternal()方法指定一个类的部分数据进行序列化与反序列化。

    private void readExternalData(Externalizable obj, ObjectStreamClass desc)
        throws IOException
    {
        SerialCallbackContext oldContext = curContext;
        curContext = null;
        try {
            boolean blocked = desc.hasBlockExternalData();
            if (blocked) {
                bin.setBlockDataMode(true);
            }
            if (obj != null) {
                try {
                    obj.readExternal(this);

在廖师傅的反序列攻击时序图,补充了一些我的理解。

[email protected]

0x03 漏洞场景

weblogic t3 反序列化举例子,可以看到基本上做修复的方式都是针对 resolveProxyClassresolveClass 进行的修复黑名单处理。

CVE-2017-3248

这个漏洞修复方式针对 resolveProxyClass ,进行 java.rmi.registry.Registry 的黑名单拦截。

protected Class<?> resolveProxyClass(String[] var1) throws IOException, ClassNotFoundException {
            String[] var2 = var1;
            int var3 = var1.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                String var5 = var2[var4];
                if (var5.equals("java.rmi.registry.Registry")) {
                    throw new InvalidObjectException("Unauthorized proxy deserialization");
                }
            }

            return super.resolveProxyClass(var1);
        }

CVE-2019-2890

这个漏洞修复方式针对 resolveClass ,进行黑名单拦截。

图片1.png

所以实际上如果应用中存在反序列化漏洞也可以参考这种方式进行拦截。

0x04 小结

作为过程记录,筛选了调用栈到反射之前的基本上所有流程记录如下:

1 Like

很不错的一篇关于java的反序列讲解,以后java不懂就找你了