前言
Java的反序列化漏洞探索出了Java安全的新紀元。開發人員爲什麼要反序列化呢?衆所周知,用戶和服務器進行交互時,會傳輸一下數據,數據傳輸前需要格式化,將數據轉化成服務器認可的格式。比例:JSON
、XML
。
JSON
和XML
的優點是兼容性比較強,是通用的數據交互格式。缺點是不支持複雜的數據類型。故開發人員面對需要複雜的數據類型是將數據反序列化,以來達數據交互的目的。
Java程序在運行時,會產生大量的數據。有些時候,我們需要將內存中的對象信息存儲到磁盤或者通過網絡發送給第三者,此時,就需要對對象進行序列化操作。當我們需要從磁盤或網絡讀取存儲的信息時,即爲反序列化。簡單理解,序列化即將內存中的對象信息轉換爲字節流並存儲在磁盤或通過網絡發送。反序列化,即從磁盤或網絡讀取信息,直接轉換爲內存對象。
PS: 爲避免代碼太長而導致的閱讀效果,故將完整的實驗代碼全部已經上傳至 https://github.com/SummerSec/JavaLearnVulnerability
反序列化demo
知識補充
反序列化漏洞基本條件
- Java反序列化類一定要實現
Serializabe
接口 - 所有的Java反序列化漏洞都是用通過
readObject()
實現 - 所有反序列化數據都是要通過
writeObject()
函數實現
SerialVersionUID
Java的序列化的機制通過判斷serialVersionUID來驗證版本的一致性。在反序列化的時候與本地的類的serialVersionUID進行比較,一致則可以進行反序列化,不一致則會拋出異常InvalidCastException。IDEA是可以自動生成一個serialVersionUID,需要設置如下。
案例DEMO
javaSerializableDemo1
源碼
public class javaSerializableDemo1 implements Serializable {
// 序列版本ID
private static final long serialVersionUID = -1877568378649280904L;
private String username;
private String password;
private Integer age;
private Integer IdCard;
private Date time;
public javaSerializableDemo1(String username, String password, Integer age, Integer idCard, Date time) {
this.username = username;
this.password = password;
this.age = age;
IdCard = idCard;
this.time = time;
}
// 省略一部分set、get方法。
@Override
public String toString() {
return "javaSerializableDemo{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
", IdCard=" + IdCard +
", time=" + time +
'}';
}
}
javaUnSerizableDemo1
源碼,一般情況下對象寫入流writerObject()
和對象的輸出流readObject
是分開實現的。
public class javaUnSerializableDemo1 {
public static void main(String[] args) {
javaSerializableDemo1 demo = new javaSerializableDemo1("Summer","6666888",18,666666,new Date());
System.out.println("serializable: " + demo);
// 將對象寫入文件中
ObjectOutputStream oos = null;
try {
FileOutputStream fileOutputStream = new FileOutputStream("tempFile.txt");
oos = new ObjectOutputStream(fileOutputStream);
// 序列化
oos.writeObject(demo);
oos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 讀文件
File file = new File("tempFile.txt");
ObjectInputStream ois = null;
try {
FileInputStream fileInputStream = new FileInputStream(file);
ois = new ObjectInputStream(fileInputStream);
// 反序列化
javaSerializableDemo1 newdemo = (javaSerializableDemo1) ois.readObject();
System.out.println("unserializable: " + newdemo);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
由於是字節碼,直接打開是亂碼。
用vscode插件hexdump
查看生成的文件。
或者使用SerializationDumper.jar
工具,效果如下部分截圖。
下載地址:https://github.com/NickstaDB/SerializationDumper
這個DEMO中實現了筆者前文所提及到的三要素,但似乎你還看不出來漏洞的存在的地方。
漏洞DEMO
下面會以一個存在的漏洞demo,帶你更進一步理解Java反序列化的危害。
漏洞源碼
public class VulnerabilityClass implements summer.serializable.Serializable {
private static final long serialVersionUID = 5550839108669505813L;
private String username;
private String password;
private Date date;
private void readObject(java.io.ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 加入執行命令代碼
Runtime.getRuntime().exec("calc");
}
public VulnerabilityClass() {
}
// 省略set、get方法
@Override
public String toString() {
return "VulnerabilityClass{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", date=" + date +
", id=" + id +
'}';
}
public VulnerabilityClass(String username, String password, Date date, Integer id) {
this.username = username;
this.password = password;
this.date = date;
this.id = id;
}
漏洞利用
public static void main(String[] args) {
// 調用序列化方法
Serilizable();
// 反序列化方法
UnSerializable();
}
public static void Serilizable(){
VulnerabilityClass clazz = new VulnerabilityClass();
clazz.setDate(new Date());
clazz.setId(1314520);
clazz.setPassword("summer6666");
clazz.setUsername("summer");
// 寫文件
File file = new File("tempFile3");
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(clazz);
System.out.println("serilizable: " + clazz);
oos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Object UnSerializable(){
File file = new File("tempFile3");
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
VulnerabilityClass clazz = (VulnerabilityClass) ois.readObject();
System.out.println("unserializable: " + clazz);
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return VulnerabilityClass;
}
}
漏洞成因分析
在漏洞源碼中的readObject()
方法,第一行是默認的反序列化方法defaultReadObject()
,但是下面一行是添加了Runtime.getRuntime().exec("calc")
,雖然這裏簡單粗暴的將執行命令的代碼寫入了方法。實際的情況下,開發人員是不會這麼做,筆者這裏簡單展示一下漏洞原理。實際情況都是攻擊者通過各種僞造方法、修改、重定義等方法最後到達執行命令。
private void readObject(java.io.ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 加入執行命令代碼
Runtime.getRuntime().exec("calc");
}
總結
反序列化漏洞三要素實現Serializabe
接口、readObject()
、writeObject()
方法,缺一不可。但試想一下,如果用戶控制了readObject
亦或者是writeObject
方法,那麼是不是可以造成反序列化漏洞呢?其實也有一個問題控制不了writeObject
方法,因爲其在服務器內,或者是Java應用內,我們不可能去修改內部代碼。所以說只能通過控制readObject
方法,這裏的控制得打雙引號。問題來了,前人大佬們已經研究出來許多控制方法,經典AnnotationInvocationHandler
和BadAttributeValueExpException
類均滿足條件,下篇文章帶你分析。
如果要深入理解反序列化漏洞可以去學習反序列化利用工具ysoserial
。網上很多反序列化文章基本上都是研究commons-collection
反序列化,但其實commons-colection
反序列化鏈是很複雜,不建議新手小白學習。ysoserial-Gadget-URLDNS
這條反序列化鏈建議新手小白學習,比較簡單。推薦文章小樓昨夜又春風,你知ysoserial-Gadget-URLDNS多少?,這篇文章全方位的解釋URLDNS
這條鏈的利用、成因,相對其他作者寫的文章分析更加全面,基本上你知道或者不知道都在文章裏面。