給個關注?寶兒!
給個關注?寶兒!
給個關注?寶兒!
1. 序列化
在調用RMI時,發現接收發送數據都是反序列化數據.
例如JSON和XML等語言,在網絡上傳遞信息,都會用到一些格式化數據,大多數處理方法中,JSON和XML支持的數據類型就是基本數據類型,整型、浮點型、字符串、布爾等,如果開發者希望在傳輸數據的時候直接傳輸一個對象,那么就不得不想辦法擴展基礎的JSON(XML)語法。比如,Jackson和Fastjson這類序列化庫,在JSON(XML)的基礎上進行改造,通過特定的語法來傳遞對象.
RMI使用java等語言內置的序列化方法,將一個對象轉化成一串二進制數據進行傳輸
2.反序列化
不管是Jackson、Fastjson還是編程語言內置的序列化方法,一旦涉及到序列化與反序列化數據,就可能會涉及到安全問題。但首先要理解的是,“反序列化漏洞”是對一類漏洞的泛指,而不是專指某種反序列化方法導致的漏洞,比如Jackson反序列化漏洞和Java readObject造成的反序列化漏洞就是完全不同的兩種漏洞。
在Java中實現對象反序列化非常簡單,實現java.io.Serializable(內部序列化)java.io.Externalizable(外部序列化)接口即可被序列化,其中java.io.Externalizable接口只是實現了java.io.Serializable接口。
反序列化類對象時有如下限制:
1.被反序列化的類必須存在。
2. serialVersionUID值必須一致。
除此之外,反序列化類對象是不會調用該類構造方法的,因為在反序列化創建類實例時使用了sun.reflect.ReflectionFactory.newConstructorForSerialization創建了一個反序列化專用的Constructor(反射構造方法對象),使用這個特殊的Constructor可以繞過構造方法創建類實例(前面章節講sun.misc.Unsafe 的時候我們提到了使用allocateInstance方法也可以實現繞過構造方法創建類實例)。
3.反序列化方法的對比
在接觸Java反序列化之前,相比大家多少都了解過其他語言的反序列化漏洞,其中極為經典的要數PHP
和Python。
那么,Java的反序列化,究竟和PHP、Python的反序列化有什么異同?
Java的反序列化和PHP的反序列化其實有點類似,他們都只能將一個對象中的屬性按照某種特定的格式
生成一段數據流,在反序列化的時候再按照這個格式將屬性拿回來,再賦值給新的對象。
但Java相對PHP序列化更深入的地方在于,其提供了更加高級、靈活地方法 writeObject ,允許開發者
在序列化流中插入一些自定義數據,進而在反序列化的時候能夠使用 readObject 進行讀取。
當然,PHP中也提供了一個魔術方法叫 __wakeup ,在反序列化的時候進行觸發。很多人會認為Java的 readObject 和PHP的 __wakeup 類似,但其實不全對,雖然都是在反序列化的時候觸發,但他們解決
的問題稍微有些差異。
Java設計 readObject 的思路和PHP的 __wakeup 不同點在于:
readObject 傾向于解決“反序列化時如
何還原一個完整對象”這個問題,而PHP的 __wakeup 更傾向于解決“反序列化后如何初始化這個對象”的
問題。
4.PHP反序列化
PHP的序列化是開發者不能參與的,開發者調用 serialize 函數后,序列化的數據就已經完成了,你得到的是一個完整的對象,你并不能在序列化數據流里新增某一個內容,你如果想插入新的內容,只有將其保存在一個屬性中。也就是說PHP的序列化、反序列化是一個純內部的過程,而其 __sleep 、 __wakeup 魔術方法的目的就是在序列化、反序列化的前后執行一些操作。
一個非常典型的PHP序列化例子,就是含有資源類型的PHP類,如數據庫連接:
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this-
>password);
}
}
PHP中,資源類型的對象默認是不會寫入序列化數據中的。那么上述Connection類的 $link 屬性在序
列化后就是null,反序列化時拿到的也是null。
那么,如果我想要反序列化時拿到的 $link 就是一個數據庫連接,我就需要編寫 __wakeup 方法:
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this-
>password);
}
public function __sleep()
{
return array('dsn', 'username', 'password');
}
public function __wakeup()
{
$this->connect();
}
可見,這里 __wakeup 的工作就是在反序列化拿到Connection對象后,執行 connect() 函數,連接數
據庫。
__wakeup 的作用在反序列化后,執行一些初始化操作。但其實我們很少利用序列化數據傳遞資源類型
的對象,而其他類型的對象,在反序列化的時候就已經賦予其值了。
所以你會發現,PHP的反序列化漏洞,很少是由 __wakeup 這個方法觸發的,通常觸發在析構函數
__destruct 里。其實大部分PHP反序列化漏洞,都并不是由反序列化導致的,只是通過反序列化可以
控制對象的屬性,進而在后續的代碼中進行危險操作。
5.Java反序列化
在Java中實現對象反序列化非常簡單,實現java.io.Serializable(內部序列化)java.io.Externalizable(外部序列化)接口即可被序列化,其中java.io.Externalizable接口只是實現了java.io.Serializable接口。
反序列化類對象時有如下限制:
1.被反序列化的類必須存在。
2. serialVersionUID值必須一致。
除此之外,反序列化類對象是不會調用該類構造方法的,因為在反序列化創建類實例時使用了sun.reflect.ReflectionFactory.newConstructorForSerialization創建了一個反序列化專用的Constructor(反射構造方法對象),使用這個特殊的Constructor可以繞過構造方法創建類實例(前面章節講sun.misc.Unsafe 的時候我們提到了使用allocateInstance方法也可以實現繞過構造方法創建類實例)。
使用反序列化方式創建類實例代碼片段:
package com.anbai.sec.serializes;import sun.reflect.ReflectionFactory;import java.lang.reflect.Constructor;/*** 使用反序列化方式在不調用類構造方法的情況下創建類實例* https://www.iteye.com/topic/850027*/
public class ReflectionFactoryTest {public static void main(String[] args) {try {// 獲取sun.reflect.ReflectionFactory對象ReflectionFactory factory = ReflectionFactory.getReflectionFactory();// 使用反序列化方式獲取DeserializationTest類的構造方法Constructor constructor = factory.newConstructorForSerialization(DeserializationTest.class, Object.class.getConstructor());// 實例化DeserializationTest對象System.out.println(constructor.newInstance());} catch (Exception e) {e.printStackTrace();}}}
輸出
6.ObjectInputStream、ObjectOutputStream
java.io.ObjectOutputStream類最核心的方法是writeObject方法,即序列化類對象。
java.io.ObjectInputStream類最核心的功能是readObject方法,即反序列化類對象。
所以,只需借助ObjectInputStream和ObjectOutputStream類我們就可以實現類的序列化和反序列化功能了。
7.java.io.Serializable
java.io.Serializable是一個空的接口,我們不需要實現java.io.Serializable的任何方法,代碼如下:
public interface Serializable {
}
您可能會好奇我們實現一個空接口有什么意義?其實實現java.io.Serializable接口僅僅只用于標識這個類可序列化。實現了java.io.Serializable接口的類原則上都需要生產一個serialVersionUID常量,反序列化時如果雙方的serialVersionUID不一致會導致InvalidClassException 異常。如果可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基于該類的各個方面計算該類的默認 serialVersionUID值。
DeserializationTest.java測試代碼如下:
package com.anbai.sec.serializes;import java.io.*;
import java.util.Arrays;/*** Creator: yz* Date: 2019/12/15*/
public class DeserializationTest implements Serializable {private String username;private String email;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public static void main(String[] args) {ByteArrayOutputStream baos = new ByteArrayOutputStream();try {// 創建DeserializationTest類,并類設置屬性值DeserializationTest t = new DeserializationTest();t.setUsername("yz");t.setEmail("admin@.com");// 創建Java對象序列化輸出流對象ObjectOutputStream out = new ObjectOutputStream(baos);// 序列化DeserializationTest類out.writeObject(t);out.flush();out.close();// 打印DeserializationTest類序列化以后的字節數組,我們可以將其存儲到文件中或者通過Socket發送到遠程服務地址System.out.println("DeserializationTest類序列化后的字節數組:" + Arrays.toString(baos.toByteArray()));// 利用DeserializationTest類生成的二進制數組創建二進制輸入流對象用于反序列化操作ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());// 通過反序列化輸入流(bais),創建Java對象輸入流(ObjectInputStream)對象ObjectInputStream in = new ObjectInputStream(bais);// 反序列化輸入流數據為DeserializationTest對象DeserializationTest test = (DeserializationTest) in.readObject();System.out.println("用戶名:" + test.getUsername() + ",郵箱:" + test.getEmail());// 關閉ObjectInputStream輸入流in.close();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}}
輸出:
核心邏輯其實就是使用ObjectOutputStream類的writeObject方法序列化DeserializationTest類,使用ObjectInputStream類的readObject方法反序列化DeserializationTest類而已。
簡化后的代碼片段如下:
// 序列化DeserializationTest類
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(t);// 反序列化輸入流數據為DeserializationTest對象
ObjectInputStream in = new ObjectInputStream(bais);
DeserializationTest test = (DeserializationTest) in.readObject();
ObjectOutputStream序列化類對象的主要流程是首先判斷序列化的類是否重寫了writeObject方法,如果重寫了就調用序列化對象自身的writeObject方法序列化,序列化時會先寫入類名信息,其次是寫入成員變量信息(通過反射獲取所有不包含被transient修飾的變量和值)。
8.java.io.Externalizable.java:
public interface Externalizable extends java.io.Serializable {void writeExternal(ObjectOutput out) throws IOException;void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;}
ExternalizableTest.java測試代碼如下:
package com.anbai.sec.serializes;import java.io.*;
import java.util.Arrays;/*** Creator: yz* Date: 2019/12/15*/
package com.anbai.sec.serializes;import java.io.*;
import java.util.Arrays;/*** Creator: yz* Date: 2019/12/15*/
public class ExternalizableTest implements java.io.Externalizable {private String username;private String email;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(username);out.writeObject(email);}@Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {this.username = (String) in.readObject();this.email = (String) in.readObject();}public static void main(String[] args) {ByteArrayOutputStream baos = new ByteArrayOutputStream();try {// 創建ExternalizableTest類,并類設置屬性值ExternalizableTest t = new ExternalizableTest();t.setUsername("yz");t.setEmail("admin@javaweb.org");ObjectOutputStream out = new ObjectOutputStream(baos);out.writeObject(t);out.flush();out.close();// 打印ExternalizableTest類序列化以后的字節數組,我們可以將其存儲到文件中或者通過Socket發送到遠程服務地址System.out.println("ExternalizableTest類序列化后的字節數組:" + Arrays.toString(baos.toByteArray()));System.out.println("ExternalizableTest類反序列化后的字符串:" + new String(baos.toByteArray()));// 利用DeserializationTest類生成的二進制數組創建二進制輸入流對象用于反序列化操作ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());// 通過反序列化輸入流創建Java對象輸入流(ObjectInputStream)對象ObjectInputStream in = new ObjectInputStream(bais);// 反序列化輸入流數據為ExternalizableTest對象ExternalizableTest test = (ExternalizableTest) in.readObject();System.out.println("用戶名:" + test.getUsername() + ",郵箱:" + test.getEmail());// 關閉ObjectInputStream輸入流in.close();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}}
輸出:
兩者之間沒有多大差別
9.自定義序列化(writeObject)和反序列化(readObject)
實現了java.io.Serializable接口的類,還可以定義如下方法(反序列化魔術方法),這些方法將會在類序列化或反序列化過程中調用:
- private void writeObject(ObjectOutputStream oos),自定義序列化。
- private void readObject(ObjectInputStream ois),自定義反序列化。
- private void readObjectNoData()。
- protected Object writeReplace(),寫入時替換對象。
- protected Object readResolve()。
具體的方法名定義在java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>),其中方法有詳細的聲明。
序列化時可自定義的方法示例代碼:
public class DeserializationTest implements Serializable {/*** 自定義反序列化類對象** @param ois 反序列化輸入流對象* @throws IOException IO異常* @throws ClassNotFoundException 類未找到異常*/private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {System.out.println("readObject...");// 調用ObjectInputStream默認反序列化方法ois.defaultReadObject();// 省去調用自定義反序列化邏輯...}/*** 自定義序列化類對象** @param oos 序列化輸出流對象* @throws IOException IO異常*/private void writeObject(ObjectOutputStream oos) throws IOException {oos.defaultWriteObject();System.out.println("writeObject...");// 省去調用自定義序列化邏輯...}private void readObjectNoData() {System.out.println("readObjectNoData...");}/*** 寫入時替換對象** @return 替換后的對象*/protected Object writeReplace() {System.out.println("writeReplace....");return null;}protected Object readResolve() {System.out.println("readResolve....");return null;}}
當我們對DeserializationTest類進行序列化操作時,會自動調用(反射調用)該類的writeObject(ObjectOutputStream oos)方法,對其進行反序列化操作時也會自動調用該類的readObject(ObjectInputStream)方法,也就是說我們可以通過在待序列化或反序列化的類中定義readObject和writeObject方法,來實現自定義的序列化和反序列化操作,當然前提是,被序列化的類必須有此方法,并且方法的修飾符必須是private。