本篇文章給大家分享了下PHP還原序列化漏洞系列之PHP序列化和還原序列化原理的相關知識,有這方面需要的朋友參考學習下吧。
0.前言
對象的序列化和還原序列化作用就不再贅述,php中序列化的結果是一個php自訂的字串格式,有點類似json.
我們在任何語言中設計對象的序列化和還原序列化都需要解決幾個問題
把某個對象序列化之後,序列化的結果有自描述的功能(從序列化的結果中知道這個對象的具體類型,
知道類型還不夠,當然還需要知道這個類型所對應具體的值).
序列化時的許可權控制,可以自訂序列化欄位等,例如golang中的做的就非常方便.
時間效能問題:在某些效能敏感的情境下,對象序列化就不能拖後腿,例如:高效能服務(我經常使用protobuf來序列化).
空間效能問題:序列化之後的結果不能太長,比如記憶體中一個int對象,序列化之後資料長度變成了10倍int的長度,那這個序列化演算法是有問題的.
本文僅僅從php代碼角度來解釋php中序列化和還原序列化的過程.,記住一點序列化和還原序列化操作的僅僅是對象的資料,這一點有物件導向開發經驗的都應該容易理解.
1.序列化serialize和還原序列化方法unserialize
php原生提供了對象序列化功能,不像c++ ……^_^. 用起來也非常簡單,就兩個介面.
class fobnn{ public $hack_id; private $hack_name; public function __construct($name,$id) { $this->hack_name = $name; $this->hack_id = $id; } public function print() { echo $this->hack_name.PHP_EOL; }}$obj = new fobnn('fobnn',1);$obj->print();$serializedstr = serialize($obj); //通過serialize介面序列化echo $serializedstr.PHP_EOL;;$toobj = unserialize($serializedstr);//通過unserialize還原序列化$toobj->print();
fobnnO:5:"fobnn":2:{s:7:"hack_id";i:1;s:16:"fobnnhack_name";s:5:"fobnn";}fobnn
看到第二行的輸出,這個字串就是序列化的結果,這個結構其實很容讀懂,可以發現是通過對象名稱/成員名稱來映射的,當然不同存取權限的成員序列化之後的標籤名稱略有不同.
根據我上面講到的3個問題,那麼我們可以來看看
1.自描述功能
O:5:"fobnn":2 其中o就表示了object類型,且類型名稱為fobnn, 採用這種格式,後面的2表示了有2個成員對象.
關於成員對象,其實也是同一套子描述,這是一個遞迴的定義.
自描述的功能主要是通過字串記錄對象和成員的名稱來實現.
2.效能問題
php序列化的時間效能本文就不分析了,詳見後面,但序列化結果其實類似json/bson定義的協議,有協議頭,協議頭說明了類型,協議體則說明了類型所對應的值,並不會對序列化結果進行壓縮.
2.還原序列化中的魔術方法
對應上述說的第二個問題,其實php中也有解決方案,一種是通過魔術方法,第二種則是自訂序列化函數.先來介紹下魔術方法 __sleep和__wakeup
class fobnn{ public $hack_id; private $hack_name; public function __construct($name,$id) { $this->hack_name = $name; $this->hack_id = $id; } public function print() { echo $this->hack_name.PHP_EOL; } public function __sleep() { return array("hack_name"); } public function __wakeup() { $this->hack_name = 'haha'; }}$obj = new fobnn('fobnn',1);$obj->print();$serializedstr = serialize($obj);echo $serializedstr.PHP_EOL;;$toobj = unserialize($serializedstr);$toobj->print();
fobnnO:5:"fobnn":1:{s:16:"fobnnhack_name";s:5:"fobnn";}haha
在序列化之前會先調用__sleep返回的是一個需要序列化的成員名稱數組,通過這樣我們就可以控制需要序列化的資料,案例中我只返回了hack_name,可以看到結果中只序列化了hack_name成員.
在序列化完成之後,會跳用__wakeup 在這裡我們可以做一些後續工作,例如重連資料庫之類的.
3.自訂Serializable介面
interface Serializable {abstract public string serialize ( void )abstract public void unserialize ( string $serialized )}
通過這個介面我們可以自訂序列化和還原序列化的行為,這個功能主要可以用來自訂我們的序列化格式.
class fobnn implements Serializable{ public $hack_id; private $hack_name; public function __construct($name,$id) { $this->hack_name = $name; $this->hack_id = $id; } public function print() { echo $this->hack_name.PHP_EOL; } public function __sleep() { return array('hack_name'); } public function __wakeup() { $this->hack_name = 'haha'; } public function serialize() { return json_encode(array('id' => $this->hack_id ,'name'=>$this->hack_name )); } public function unserialize($var) { $array = json_decode($var,true); $this->hack_name = $array['name']; $this->hack_id = $array['id']; }}$obj = new fobnn('fobnn',1);$obj->print();$serializedstr = serialize($obj);echo $serializedstr.PHP_EOL;;$toobj = unserialize($serializedstr);$toobj->print();
fobnnC:5:"fobnn":23:{{"id":1,"name":"fobnn"}}fobnn
當使用了自訂序列化介面之後,我們的魔術方法就沒用了.
4.PHP動態類型和PHP還原序列化
既然上文中提到的自描述功能,那麼序列化結果中儲存了對象的類型,且php是動態類型語言,那麼我們就可以來做個簡單的實驗.
class fobnn{ public $hack_id; public $hack_name; public function __construct($name,$id) { $this->hack_name = $name; $this->hack_id = $id; } public function print() { var_dump($this->hack_name); }}$obj = new fobnn('fobnn',1);$obj->print();$serializedstr = serialize($obj);echo $serializedstr.PHP_EOL;;$toobj = unserialize($serializedstr);$toobj->print();$toobj2 = unserialize("O:5:\"fobnn\":2:{s:7:\"hack_id\";i:1;s:9:\"hack_name\";i:12345;}");$toobj2->print();
我們修改hack_name還原序列化的結果為int類型, i:12345
string(5) "fobnn"O:5:"fobnn":2:{s:7:"hack_id";i:1;s:9:"hack_name";s:5:"fobnn";}string(5) "fobnn"int(12345)
可以發現,對象成功序列化回來了!並且可以正常工作!. 當然php的這種機制提供了靈活多變的文法,但也引入了安全風險. 後續繼續分析php序列化和還原序列化特性帶來的安全問題.