靶场地址
CTFhub → 历年真题 → Web → 2018-HCTF-Web-warmup
在题目列表里筛选 Web、难度困难,点进 warmup。

一句话漏洞点
checkFile() 校验的是 ? 截断后的字符串,include 用的却是原始字符串,校验与执行不一致 → 白名单绕过 + 目录穿越。
看页面源码:注释里留下了 source.php
view-source: 打开首页,第 10 行是 <!--source.php-->,这是审计入口。

访问 source.php:白名单只放行两个文件名
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];白名单里除了 source.php 还有 hint.php,而 hint 就是提示的意思 → 先去看 hint.php。

访问 hint.php,得到 flag 的文件名
flag not here, and flag in ffffllllaaaagggg
回来分析拦截逻辑:校验被 ? 截断,include 没截断

$_page = mb_substr($page, 0, mb_strpos($page . '?', '?')); // 只取第一个 ? 之前的部分
if (in_array($_page, $whitelist)) { return true; } // 校验的是这一段
...
include $_REQUEST['file']; // 执行的是完整原始参数mb_strpos($page . '?', '?'):在参数后补一个?再找位置,所以一定能找到,返回值就是第一个?的下标。mb_substr($page, 0, 该下标):把?及其后面全部砍掉,只拿前面送去比对。- 于是只要前半段是白名单文件名、
?后面接任意内容,校验就一定通过,而include照样带着后半段去加载文件。
构造 payload:前半段过校验,后半段穿越目录
http://challenge-055d9b46127e5bc0.sandbox.ctfhub.com:10800/source.php?file=source.php?../../../../../ffffllllaaaagggg
file=source.php?../../../../../ffffllllaaaagggg- 校验阶段:截到第一个
?→source.php,命中白名单 → 返回 true; - 执行阶段:
include拿到完整字符串,../逐级上跳,命中上面从 hint.php 得到的文件名ffffllllaaaagggg。
为什么是 ../../../../../ffffllllaaaagggg,而不是 ffffllllaaaagggg 或 ../ 开头
../ 不是装饰,它决定 include 最终去哪个目录找文件:
| 写法 | 实际查找位置 | 结果 |
|---|---|---|
ffffllllaaaagggg | 当前工作目录下,即 /var/www/html/ 里 | 文件不在这,失败 |
../ffffllllaaaagggg | 只上跳 1 级(/var/www/) | 深度不够,失败 |
source.php?ffffllllaaaagggg | source.php?ffffllllaaaagggg 被当成目录名 | 失败 |
../../../../../ffffllllaaaagggg | 先上跳 5 级再找 | 成功 |
原因有三个:
- flag 文件不在网站根目录。hint.php 只说"flag in ffffllllaaaagggg",没给路径;直接请求
http://.../ffffllllaaaagggg报 403 Forbidden,说明它位于 Web 根目录之外,必须靠目录穿越走出去。 ../的层数是"猜深度",不是精确指定。../只能上一级,而当前工作目录的绝对路径并不知道;../../../../../是上跳够多次直到文件系统根/,之后/ffffllllaaaagggg就是绝对路径。层数取多不取少——上跳超过根目录后仍停在/,所以多写几层不会出错,5~7 层是覆盖常见镜像深度的通用写法。- 前面的
source.php?会变成一个不存在的目录名。因为校验要求开头必须是白名单文件名,payload 前半段被钉死成source.php?,它在路径里等同于一个目录;这更说明必须依赖"上跳够多次回到根"来保证最终命中。
一句话概括:ffffllllaaaagggg 来自 hint.php(信息),../ 负责把它从"目录未知"变成"绝对路径可达"(利用),两者缺一不可。
拿到 flag
ctfhub{5517caa381efd9d6375a9a33}校验与使用不一致是什么
漏洞根子是检查的字符串和执行时用的字符串不是同一个:校验前先按 ? 截断(只看文件名),执行时却把原始参数整串交给 include(路径部分照样生效)。判断特征很好认——源码里出现"截断后再 in_array 白名单"的写法,基本都能这样绕。
修复方式:校验和使用同一个变量,用 basename() 取纯文件名与白名单精确比对,再用 realpath() 把结果限制在指定目录内,不要依赖 ? 截断做安全判断。
补充
- 截断条件不限于
?:urldecode之后还会再截一次,所以传source.php%3f../../..也能过校验,效果相同。 - 若白名单里没有 hint.php,就得另想办法猜文件名,这道题的 hint.php 相当于直接给了答案的一半。
评论 (0)