PHP反序列化字符串逃逸
PHP反序列化字符串逃逸这个知识点稍微的有点难懂,以前其实是会的但是前几天遇到过这样的题目有点迷糊所以还是要形成文章比较好。
基础思想
在理解了php反序列化后你就能知道字符串逃逸本质上是闭合,类似于sql注入中利用分号或者引号来闭合语句。反序列化字符串逃逸也是利用php对序列化字符串解析的特性来进行攻击。
在反序列化时,序列化的值是以分号作为分隔,在结尾以}为结束。我们看一个例子:
<?php
class people{
public $name = 'Tom';
public $sex = 'boy';
public $age = '12';
}
$a = new people();
print_r(serialize($a));
?>
运行结果:
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}
反序列化的过程就是当碰到与{最接近的;}时完成匹配并停止解析,我们将上列反序列化结果的结尾加上一些无意义的字符串并反序列化。
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}123123
经过反序列化后:
可以看到我们在结尾添加的123123并没有影响反序列化,程序没有报错解析结果也没有问题。这说明上述的原理是正确的。
但是当我修改字符串长度的数值时,我们来看看反序列化还能否正常解析,例如我将s:2:"12"
改为s:3:"12"
:
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:3:"12";}
php报错:
PHP Notice: unserialize(): Error at offset 75 of 77 bytes in /Users/oriole/WorkSpace/www/html/index.php on line 16
实际上,在解析字符串的过程中,当字符串长度与反序列化描述不同时php将报错,换一种说法:php将按照反序列化字符串定义的长度读取字符串,当引号内的定义长度的字符串在读取时未读取至末尾引号之前的字符、或超越了末尾引号进行读取时就会报错。
因为这种报错的存在,字符串逃逸分为两种:
-
关键字增多
-
关键字减少
关键字增多
在题目中,往往对一些关键字会进行一些过滤,使用的手段通常替换关键字,使得一些关键字增多,简单认识一下,正常序列化查看结果。
<?php
show_source(__FILE__);
$a=array("username"=>"Tom","age"=>"13");
$b=serialize($a);
echo $b;
echo "<br/>";
$c=preg_replace('/o/',"oo",$b);
echo $c."<br/>";
print_r(unserialize($c));
?>
输出:
a:2:{s:8:"username";s:3:"Tom";s:3:"age";s:2:"13";}
a:2:{s:8:"username";s:3:"Toom";s:3:"age";s:2:"13";}
PHP Notice: unserialize(): Error at offset 28 of 51 bytes in /Users/oriole/WorkSpace/www/html/index.php on line 11
在上述代码中,未反序列化的序列化字符串中的所有o将被替换为oo,这必然破坏了序列化字符串原有的规则,因而必然引起报错,而我们的目的就是构造字符串使其不要引起报错,为了更易理解,我们增加一个需求:age字段修改为35。
Payload:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
原理:在经过preg_replace以后,每个o都会被替换为oo,比如说我们传入s:3:"Tom"
会被替换为s:3:"Toom"
,这样必然报错。我们要做的就是让这个字符串在增长以后真的有它描述的那么长,同时达到我们将age字段修改为35的目的。
而实现这一目的的方法就是使反序列化结构包含在这一字符串内,当字符串经过替换后,使其长度等于原来包含反序列化结构的字符串长度,从而使之正常被作为字符串解析的同时使包含在字符串内的反序列化结构被php识别为反序列化结构进行解析,同时冗余的反序列化结构(也就是完整{;}结构以外的)被遗弃。 可能乍一看难以理解,可以选择再仔细理解一下基础思想或继续向下看构造payload的每一个过程。
我们还以上述代码为例,我们需要在字符串中构造一个反序列化结构同时还要达到在替换后闭合前侧字符串的目的:";s:3:"age";s:2:"35";}
,而这一反序列化结构的长度是22:
也就是说我们需要22个字符来代替其位置,每一个o会被替换为oo也就是增加一个字符,也就是说我们需要22个o来增加22个字符的长度。
因此我们将22个字符长度的oooooooooooooooooooooo
与";s:3:"age";s:2:"35";}
组成username变量的值,我们将其序列化后得到:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
在被preg_replace替换后,长度为22的oooooooooooooooooooooo
变为长度为44的oooooooooooooooooooooooooooooooooooooooooooo
,整个反序列化的payload也变为:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
当PHP反序列化这个payload时s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";
将被作为整体解析,而";s:3:"age";s:2:"35";}
在最前面的引号与分号闭合了前面的变量后的s:3:"age";s:2:"35";}
也会被正常解析。而因为已经满足了完整的{;}
结构";s:3:"age";s:2:"13";}
将被遗弃而不被解析。
因而可以看到如下的反序列化结果:
Array ( [username] => oooooooooooooooooooooooooooooooooooooooooooo [age] => 35 )
这就完成了一次成功的‘逃逸’。
关键字减少
如果上述内容理解的还可以,那么关键字减少的部分理解起来不会很难。和关键字增多的区别在于通常会把需要反序列化的结构放在关键字增多中冗余抛弃的那一部分,而关键字增多这一部分中我们希望通过关键字符串增多而解析的反序列化结构在关键字减少中需要因字符串长度变化而成为关键字符串的一部分从而不被解析。
和关键字增多的例子相似:
<?php
show_source(__FILE__);
$a=array("username"=>"Zoo","age"=>"15");
$b=serialize($a);
echo $b;
echo "<br/>";
$c=preg_replace('/oo/',"o",$b);
echo $c."<br/>";
print_r(unserialize($c));
?>
在这一个例子里oo会被替换为o,假设我们的目的仍然是将age的值改为35。
payload:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";s:3:"age";s:2:"15";}";s:3:"age";s:2:"35";}
可以看到age被改为了35。
实现这一目的的原理和关键字增多类似:;s:3:"age";s:2:"15";}
长度是22,我们构造44个o来作为username的值,然后将我们需要更改的反序列化结构;s:3:"age";s:2:"35";}
放在最后面,经过preg_replace替换后o的数量变为22,而username所定义的长度为44,这时php会将;s:3:"age";s:2:"15";}
这22个字符一并作为username的一部分进行读取,而后我们后面部分只要闭合的好,就会被作为正常的反序列化结构进行解析,从而达到逃逸的目的