Java反序列化基础

Java反序列化基础

1.扫盲

  • 常用单词

    source:入口,gadget chain:调用链,sink:目标方法,RMI:remote method invocation远程方法调用

  • 反序列化漏洞的理解

    反序列化漏洞的挖掘,本质上就是已知source和sink,如何走通整个流程的问题

    这里source包括:

    • Java原生的反序列化,,即通过objectInputStream.readObject(),处理二进制格式的内容得到java对象
    • 专有格式的反序列化,列如通过Fastjson、Xstream等第三方库,处理json、xml等格式内容,得到java对象

    而要执行的sink包括:

    • Runtime.exec()直接在目标环境中执行命令
    • Method.Invoke()这种需要选择合适的方法和参数,通过反射执行方法
    • RMI/JNDI/JRMP等,通过引用远程对象,间接命令执行

2.ObjectOutputStream序列化分析

示例(来自https://y4er.com/post/java-deserialization-1/)

import java.io.*;

interface Animal {
    public void eat();
}

class Ani implements Serializable {
    public String name;

    private void writeObject(java.io.ObjectOutputStream out) throws IOException, ClassNotFoundException{
        out.defaultWriteObject();
        System.out.println("重新writeObject");
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("calc");
    }
}

class Cat extends Ani implements Animal {
    @Override
    public void eat() {
        System.out.println("cat eat.");
    }
}

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Ani cat = new Cat();
        cat.name = "tom";
        FileOutputStream fos = new FileOutputStream("obj");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(cat);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        Cat objectFromDisk = (Cat) ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

ObjectOutputStream的writeObject调用栈:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->defaultWriteFields或者invokeWriteObject

PS:如果对象重写了writeObject则会通过invokeWriteObject反射调用重写的writeObject,反之直接调用defaultWriteFields进行序列化
writeObject:

    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }

如果ObjectOutputStream是有参构造器的话enableOverride = false;

writeObject0关键代码
代码1

// check for replacement object
            Object orig = obj;
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
            }
            if (enableReplace) {
                Object rep = replaceObject(obj);
                if (rep != obj && rep != null) {
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                obj = rep;
            }

desc存放的是当前类的描述信息
代码2

if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "
" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }

如果对象是Serializable则进入writeOrdinaryObject

writeOrdinaryObject

    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                (depth == 1 ? "root " : "") + "object (class "" +
                obj.getClass().getName() + "", " + obj.toString() + ")");
        }
        try {
            desc.checkSerialize();

            bout.writeByte(TC_OBJECT);
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

writeSerialData

    private void writeSerialData(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 (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class "" +
                        slotDesc.getName() + "")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

这里面slotDesc.hasWriteObjectMethod()判断了是否重写writeObject,重写之后通过反射调用重写的writeObject,否则直接进入defaultWriteFields

示例运行结果

成功调用了我们重写的writeObject

3.ObjectInputStream反序列化分析

示例(来自https://y4er.com/post/java-deserialization-1/)

import java.io.*;

interface Animal {
    public void eat();
}

class Ani implements Serializable {
    public String name;

    private void writeObject(java.io.ObjectOutputStream out) throws IOException, ClassNotFoundException{
        out.defaultWriteObject();
        System.out.println("重新writeObject");
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("calc");
    }
}

class Cat extends Ani implements Animal {
    @Override
    public void eat() {
        System.out.println("cat eat.");
    }
}

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Ani cat = new Cat();
        cat.name = "tom";
        FileOutputStream fos = new FileOutputStream("obj");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(cat);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        Cat objectFromDisk = (Cat) ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

反序列化调用栈
已示例为例

writeObject--->writeObject0--->readOrdinaryObject--->readSerialData--->invokeReadObject

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 (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

readObject0代码

    private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        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:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

readObject0中根据对象不同类型调用不同的方法,这里调用的是readOrdinaryObject

readOrdinaryObject关键代码

Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

从desc中获取对象实例后调用readSerialData

readSerialData中判断了是否重写readObject方法,如果重写则使用反射调用重写的readObject,反之使用默认的方法进行反序列化
readSerialData

    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()) {
                    ThreadDeath t = null;
                    boolean reset = false;
                    SerialCallbackContext oldContext = curContext;
                    if (oldContext != null)
                        oldContext.check();
                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
                        /*
                         * In most cases, the handle table has already
                         * propagated a CNFException to passHandle at this
                         * point; this mark call is included to address cases
                         * where the custom readObject method has cons'ed and
                         * thrown a new CNFException of its own.
                         */
                        handles.markException(passHandle, ex);
                    } finally {
                        do {
                            try {
                                curContext.setUsed();
                                if (oldContext!= null)
                                    oldContext.check();
                                curContext = oldContext;
                                reset = true;
                            } catch (ThreadDeath x) {
                                t = x;  // defer until reset is true
                            }
                        } while (!reset);
                        if (t != null)
                            throw t;
                    }

                    /*
                     * defaultDataEnd may have been set indirectly by custom
                     * readObject() method when calling defaultReadObject() or
                     * readFields(); clear it to restore normal read behavior.
                     */
                    defaultDataEnd = false;
                } 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);
                }
            }
        }
            }

之前的疑问

  • 为什么示例中的命令执行不能直接拿来利用?

通过对Java反序列化的JDK代码分析可知,反序列化首先会获取利用对象的描述(desc),通过反射的方式执行readObject。这里反射执行的是当前环境的那个利用类和readObject,不是直接执行序列化对象的readObject,而开发者的代码中不会这样去写readObject,所以在真实攻击中无法利用成功。

原文地址:https://www.cnblogs.com/g0udan/p/14914804.html