题目描述

进入题目是一个网页,查看页面源码的注释中发现 source.php,访问后得到完整的 PHP 源代码:

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
41
42
43
44
45
46
47
48
49
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php", "hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

代码分析

白名单中只允许两个文件:source.phphint.php

checkFile 函数对传入的 file 参数做了三层检查:

检查 处理方式 绕过思路
检查1 直接 in_array($page, $whitelist) 必须精确匹配白名单
检查2 在第一个 ? 处截断,再匹配白名单 可以用 source.php?xxx 绕过,但include时 ? 后面的内容会被当作查询字符串丢弃
检查3 urldecode,再在 ? 处截断匹配 利用双重URL编码,使解码后截断匹配白名单,但原始值指向目标文件

如果三个检查都通过不了,返回 false。如果任意一个通过,就会 include $_REQUEST['file']

关键点:

  • 检查2 可以用 source.php?/../../flag 绕过,但PHP的 include 在遇到 ? 时会丢弃后面的内容,所以只能包含白名单文件本身
  • 检查3 多了一步 urldecode,我们需要利用这个”额外解码”来做文章

理解两次URL解码

整个利用的核心在于 PHP 对参数进行了两次 URL 解码,且检查与 include 用的是不同的解码结果:

阶段 发生在哪 结果 用于
第①次解码 PHP解析 $_GET/$_REQUEST 时自动完成 %25%%253F%3F(字面字符) include $_REQUEST['file']
第②次解码 checkFile 中手动调用 urldecode($page) %3F?(真正的问号) ? 处截断匹配白名单

漏洞本质:检查时用的是第②次解码后的值(含 ? 可截断匹配白名单),而 include 用的是第①次解码后的值(%3F 仍是字面字符,作为普通路径组件参与目录遍历)。两次解码结果不同,造成了绕过空间。

寻找提示

首先访问 hint.php
666

得到提示,flag 在名为 ffffllllaaaagggg 的文件中。

漏洞利用思路

利用检查3的双重URL编码绕过:

  1. 我们需要传入一个值 V,使得:

    • urldecode(V)hint.php? 开头 → 截断后匹配白名单 → 通过检查3
    • V 本身是一个有效的路径,指向 ffffllllaaaagggg
  2. 关键构造:

    • 在URL中传入 hint.php%253F/../../../../ffffllllaaaagggg
    • %25% 的URL编码,所以PHP一次解码后得到 hint.php%3F/../../../../ffffllllaaaagggg
    • 这里 %3F字面字符 %3F,不是 ?

检查过程逐级分析

检查1 — 直接 in_array
"hint.php%3F/../../../../ffffllllaaaagggg" 不在白名单中 → ❌

检查2 — 在 ? 处截断:
mb_strpos("hint.php%3F/../../../../ffffllllaaaagggg?", "?")
字符串中没有字面 ?%3F 是三个普通字符),找到的是追加在末尾的 ?
→ 截断后得到完整字符串,不在白名单中 → ❌

检查3 — URL解码后在 ? 处截断:
urldecode("hint.php%3F/../../../../ffffllllaaaagggg")
"hint.php?/../../../../ffffllllaaaagggg"
→ 在 ? 处截断得到 "hint.php" → 在白名单中 → ✅ 通过!

Include 时的路径解析

include "hint.php%3F/../../../../ffffllllaaaagggg"

PHP 内部对包含路径做归一化处理,将 .. 作为目录回溯解析:

1
/var/www/html/hint.php%3F/../../../../ffffllllaaaagggg
  1. hint.php%3F — 路径组件
  2. .. → 回到 /var/www/html/
  3. ../var/www/
  4. ../var/
  5. ../
  6. ffffllllaaaagggg → 拼入路径

最终路径:/ffffllllaaaagggg → 成功读取 flag!

攻击过程

666

拿到 Flag:

1
DASCTF{5edecbeb-fe51-49ba-9f80-6ed1ce06e32f}

总结

这道题考察的是 白名单文件包含 + 双重URL编码绕过 的经典组合:

  1. 第一层:直接用 ? 截断可以绕过检查2,但include时 ? 后的内容被丢弃,无法实现任意文件读取
  2. 第二层:检查3有额外的 urldecode,通过将 ? 双重编码(%253F),让解码后的字符串能通过白名单检查,而原始值(一次解码后的 %3F)在include时作为普通字符参与路径解析
  3. 利用 PHP 对 include 路径的归一化机制,用 ../ 回溯目录,最终包含到根目录下的目标文件

这种绕过模式在 CTF 白名单文件包含题目中非常经典,核心就是利用 “解码前后的值不同” —— 检查时用解码后的值匹配白名单,包含时用解码前的值作为文件路径。