记录一下我做 HCTF 2018 WarmUp 这道题时的理解过程,主要是搞懂几个一开始卡住我的问题。
题目背景
打开靶机只有一个滑稽图。
查看源码发现注释提示source.php。访问后拿到源码,访问hint.php得到提示:flag 在ffffllllaaaagggg文件里。
核心代码(简化):
classemmm{publicstaticfunctioncheckFile(&$page){$whitelist=["source"=>"source.php","hint"=>"hint.php"];// ... 若干检查 ...// 每次检查通过就 return true,全部失败 return false}}if(!empty($_REQUEST['file'])&&is_string($_REQUEST['file'])&&emmm::checkFile($_REQUEST['file'])){include$_REQUEST['file'];// ← 真正读文件的地方}else{echo"<img src=滑稽图/>";// ← 检查不过就显示图片}我的几个疑问与解答
疑问 1:checkFile 只是返回 true,flag 是怎么拿到的?
一开始我卡在这里:函数只是返回一个 true,那 flag 到底怎么出来?
搞懂后发现是三者配合,缺一不可:
- checkFile 的 true:唯一作用是"骗过安检",让程序走到
include那一行(否则只显示滑稽图)。它自己不读任何东西。 - include:才是真正"读文件"的动作。它把参数当文件路径,读出内容显示。
../:不是被执行的代码,是文件路径里的"上一级目录"记号,操作系统靠它一层层往上找文件。
传入: ?file=hint.php?/../../../../ffffllllaaaagggg ↓ checkFile 用 "?" 截取出 "hint.php" → 在白名单 → 返回 true(拿到门票) ↓ include 拿到的却是完整原路径 hint.php?/../../../../ffffllllaaaagggg ↓ 操作系统解析 "../" 一层层往上 → 定位到根目录的 ffffllllaaaagggg → 输出 flag关键漏洞点:checkFile 检查的是截取后的值(hint.php),而 include 用的是截取前的原值。而且函数内部只把结果赋给$_page,从没改过$page,所以外面的$_REQUEST['file']一直保持原样。
疑问 2:../是怎么"执行"的?
../从不被"执行"。它是路径的一部分,给操作系统看的导航:
include "hint.php?/../../../../ffffllllaaaagggg" 起点 /var/www/html hint.php? → 进入这个"目录" ../ → 退回 html ../ → www ../ → var ../ → 根目录 / ffffllllaaaagggg → 目标文件!说明:
hint.php?目录并不存在,但操作系统会把某目录/../成对抵消掉(路径归一化),所以不用等它真实存在。这也解释了为什么要 4 个../:1 个抵消hint.php?/,3 个从 html 爬到根目录。
疑问 3:为什么 payload 前面要加file=?
因为代码用$_REQUEST['file']取值。file=是"收件人名字",必须和代码里的'file'对上:
?file=hint.php?/../.. → $_REQUEST['file'] = "hint.php?/../.." ↑名字 代码里的 ['file'] 取这个名字名字写错(如写成page=),PHP 就放进别的抽屉,代码取不到,等于白传。
疑问 4:file前面那个?是因为 checkFile 检查两次吗?
不是。payload 里其实有两个?,作用完全不同:
http://目标/source.php ? file = hint.php ? /../../../../ffffllllaaaagggg ↑ ↑ ①号 ? ②号 ?- ①号
?:URL 语法的固定标点,分隔"路径"和"参数",所有网址都这样写,和 checkFile 无关。去掉它服务器就不认file=...是参数。 - ②号
?:漏洞利用自己构造的,给 checkFile 当截断点,截出hint.php骗白名单。这个才和检查有关。
疑问 5:为什么我加的%23(#)不对?
我曾经写hint.php?%23/../../../,想用#当分隔符。错在两点:
#在 URL 里是锚点标记,浏览器会把#后面的内容截掉不发给服务器。服务器只收到hint.php?,../全丢,include 找不到文件。- 根本不需要
#:?本身就是分隔符,后面直接接/../就行。
正确写法:hint.php?/../../../../ffffllllaaaagggg
完整 Payload 与结果
http://<靶机>/source.php?file=hint.php?/../../../../ffffllllaaaaggggCTF2{6cf95f90-479b-4844-83dc-f388f6e9c43a}
../层数看靶机目录深度,本环境 4 层刚好,实际可多试几层。
一句话总结
用
?骗 checkFile 说"我要访问 hint.php"(拿到 true 的门票),
但 include 真正读的还是那串带../的完整路径,
操作系统顺着../一层层往上爬,把根目录的 flag 读了出来。
学习记录,欢迎指正。