以前在学习PHP反序列化字符逃逸的时候,找不到合适的文章来加深我对它的理解,现在写一篇方便后续拿来快速回忆;我这里用例题详细讲解了PHP反序列化中字符逃逸漏洞中字符增加和字符减少的两种情况

字符增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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));
?>

php过滤函数会把P替换成两个W,导致字符串长度改变,S的值对应不上字符串真实长度
i:0=username i:1=age

例如
a:2:{i:0;s:7:"purplet";i:1;s:2:"10"} ->
a:2:{i:0;s:7:"WWurWWlet";i:1;s:2:"10"}
a的值代表里面有两个数组、i代表第几个数组、s代表字符串长度和它的值

这样第一个数组 i:0;s:7:"WWurWWlet"; 字符串长度s的值还是7,但是其实是9,相当于只要传一个p,就会多出一个字符。

思路

这样的话就会出现一个思路:例如,如果我们给username传进去的值长度为32位,组合为16个p再加上16个任意字符,那么在经过filter函数过滤后,16个p就会变成32个W,这样就满足了刚刚在username传进去的32位,如下:
a:2:{i:0;s:32:"WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW+16个其他字符";i:1;s:2:"10"}

这样的话就逃逸出16位字符,我们只需要加上一个双引号,就可以直接把 s:32:"32个W ,给闭合掉,再自己构造好i:1的age值并用}把真正的i:1闭合掉即可。

总结
该思路仅为了方便理解,在实践时,应该提前构造好用来闭合的payload,再计算payload的总长度。闭合payload为16位是前因,传16个p是后果。

传p的个数为:
构造闭合payload的长度除以多出的字符数

例如,闭合payload为16位,那么就传入username为16个p(p变成WW,增加一位字符串的情况)+16位的payload总共32位。

实践

这道题的目的是把年龄10改成20

先构造好闭合payload
";i:1;s:2:"20";}
他的长度是16位,想要把他传进去并闭合掉后面的age的值,就需要在username里面传入构造好的poyload

由于上面已经说了,每传进去一个p就会多出一个字符,我们构造好的数组是16位,因此就要传16个p,payload如下,并从username中传入

1
pppppppppppppppp";i:1;s:2:"20";}

得到结果

逐步解读

正常没有过滤函数的序列化会变成如下


它s的32代表 pppppppppppppppp";i:1;s:2:"20";} ;16个p加上长度为16的 ";i:1;s:2:"20";}

有过滤函数的序列化会变成如下

pppppppppppppppp";i:1;s:2:"20";} ——>WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW";i:1;s:2:"20";}

它s的32代表 32个W

32个W把S:32填充完毕,由于payload中p后边加上了双引号闭合,正好把i:0和i:1分开了,i:1就是我们传入的;i:1;s:2:”20”;} 。后面真正的i:1就被我们闭合掉了,就没有作用了

反序列化后的结果


这样就成功把age的值从10变成20了

注意点

这里的双引号其实就是i:0的,和真正的i:1都被闭合掉了

画横线的本质上是属于i:0 的 但是由于字符串增加了16位,总共48位

在反序列化过程中,由于S:32的限制 它只会从第一个W往后读32位就会结束,后面的就不管了,导致后面的;i:1;s:2:”20”;}被当成真的,并闭合掉了i:1;s:2:”10”}

总结

遇到字符增加的反序列化
1.先判断增加了几个字符(一个p变成一个WW,增加了一个)
2.构造要传入payload长度(";i:1;s:2:"20";}总共16位)
3.计算要传入字符(p)的个数
4.和p一块传入即可,需要注意要传到哪个参数。

字符减少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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));
?>

思路

如果字符增加,就让它填充,同理我们在username传p,如果字符减少,那就让它向后吞噬,把真正的i:1数组前半部分吞掉,再从age参数传入一个我们提前构造好的age数组,就可以完成字符逃逸。

例如,在过滤函数过滤之前为,a:2:{i:0;s:7:”ppurlet”;i:1;s:2:”10”;}
过滤后变为 a:2:{i:0;s:7:”Wurlet”;i:1;s:2:”10”;} 而s:7:”Wurlet”中Wurlet长度是六位,在反序列化过程中,它就会继续往下读一位把单引号吃掉,因而导致了反序列化错误。

但是这也给我们引出了思路,在username参数多传几个p让s的值变大,从而继续往下吞噬。
因为还不确定要吞噬多少位,先随便传值看看


可以看到我们只要把12位";i:1;s:16:"吞噬掉,再给age参数传入一个我们新写好的i:1就可以了。

注意点


这里的";}"不用管,后续会把它闭合掉

实践

payload如下

1
2
username=pppppppppppppppppppppppp
age=";i:1;s:2:"20";}

payload解读

传入24个p的原因:因为每传两个p就会吞噬掉一个字符,我们需要吞噬掉";i:1;s:16:"总共十二个字符,因此传入24个p

age参数";i:1;s:2:"20";}中写";的原因:用来闭合掉s:24:"的双引号,分号后就是我们新写入的i:1了;最后面的花括号就是用来闭合前文注意点所提到的a:2:{的。

这样传入并反序列化后就已经成功写了age的值了。

最终结果

发现

文章写到最后发现,其实我这里payload(”;i:1;s:2:”20”;})中不用写双引号也行,前文是要吞噬掉";i:1;s:16:",我们只吞噬掉";i:1;s:16:也行,最后这个双引号留着,用来闭合S:24:”的双引号,这样就不用再age中写双引号了。

把p减少两个,并把age中的双引号删除

反序列化也能成功

总结

遇到字符减少时的反序列化
1.先判断减少了几个字符(两个p变成一个W,减少了一个)
2.找到需要吞掉字符串的长度(本例题是i:1的前大半部分)
3.计算要传入字符(p)的个数
4.构造好要修改的数组并传入即可,需要注意要传到哪个参数。