# Java反序列化过程深究

**URL:** <https://forum.90sec.com/t/topic/599>\
**Category:** 技术文章\
**Created:** [2019 年11 月 20 日 08:08 UTC](https://forum.90sec.com/t/topic/599 "2019-11-20T08:08:19Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

作者： ![l1nk3r](https://forum.90sec.com/user_avatar/forum.90sec.com/l1nk3r/32/1212_2.png) [@l1nk3r](https://forum.90sec.com/u/l1nk3r)\
发布日期： [2019 年11 月 20 日 08:08 UTC](https://forum.90sec.com/t/topic/599/1 "2019-11-20T08:08:19Z")

</div>

## 0x01 概述

互联网上大家针对Java反序列化的讨论有很多了，但是这里我还是想聊聊，这里仅仅记录一下自己的学习笔记，之前我在[Java 反序列化深究](http://www.lmxspace.com/2019/06/27/Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B7%B1%E7%A9%B6/)中讨论了为什么，通过重写 **readObject** 方法会导致反序列化的问题，当然后续整个过程我们也没继续，当然现在继续回来讨论这个事情。

## 0x02 反序列化过程

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/e/ef1ccaad7ce7702efdab5ff8f41e301e8e1931b7.jpeg) image-20191120103607464.png

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

```auto
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** ，会根据 **tc** 的 **byte** 值进入 **switch** 中，选择相关的 **case** 进行下一步操作。回到之前反序列化字节流中，我们可以看到这里的 **TC\_OBJECT** 为 **0x73** ，所以这里会进入 **case** 为 **TC\_OBJECT** 中 **readOrdinaryObject** 的进行字节流的处理。

```auto
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

```

 ![image](https://forum.90sec.com/uploads/default/original/2X/5/583310874c3966c2cf26224c6b5369566d351b10.png)

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

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

```

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

```java
//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));
        }
    }

```

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

```java
    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](https://forum.90sec.com/uploads/default/original/2X/b/baad382f78b85ec04717608f8bda53db1b579da8.png) image-20191120110136619.png

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

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

```

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

```java
    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** 来处理，比如获取代理类这些操作。

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

```

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

```java
    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** 型。

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

```

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

```java
//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** 类。

```java
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** 。

```java
 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** ，这个属性我们前面说过了。

```java
    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** 方法，可以看到他会对一些属性进行判断。

```java
    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;
        }
    }

```

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

```java
readObjectMethod = getPrivateMethod(cl, "readObject",new Class<?>[] { ObjectInputStream.class },

```

- 方法名为readObject
- 返回类型为void
- 传入参数为一个ObjectInputStream.class类型参数
- 修饰符不能包含static
- 修饰符必须包含private

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

```java
    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** 实际上再往下走就是一些反射方法了。

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

```

再回到 **ObjectInputStream#readSerialData** 中，我们讨论了 **slots[i].hasData** 为 **true** 的情况下，实际上当这个 **if** 为 **false** 的时候来到的是 **slotDesc.invokeReadObjectNoData** 。

```java
    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** 来进行反序列化。

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

```

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

```java
    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()方法指定一个类的部分数据进行序列化与反序列化。**

```java
    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);

```

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/1/1452761f77b35bc826b20df1682a4a73c74175ec.jpeg) WX20191120-132236@2x.png

## 0x03 漏洞场景

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

### CVE-2017-3248

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

```java
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** ，进行黑名单拦截。

 ![image](https://forum.90sec.com/uploads/default/original/2X/b/bed2e7eaf970738b4e5b3dc1619ddae18bd41373.png) 图片1.png

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

## 0x04 小结

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

 ![image](https://forum.90sec.com/uploads/default/original/2X/1/181a1d9a4632a333e2813bf07f9a91d474476b09.png)

---

<div class="post-metadata">

作者： ![Joseph](https://forum.90sec.com/letter_avatar_proxy/v4/letter/j/dc4da7/32.png) [@Joseph](https://forum.90sec.com/u/Joseph)\
发布日期： [2019 年11 月 20 日 10:55 UTC](https://forum.90sec.com/t/topic/599/2 "2019-11-20T10:55:02Z")

</div>

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