WeChall - PHP 0816
PHP0816 Challenge - The Highlighter 一个带白名单的 PHP source highlighter。目标是绕过
src白名单,读到solution.php。
Challenge
PHP 0816 是 PHP 0815 后面的一个小型 Web/PHP 题,难度 4.10。页面给了几个链接:
solution.php:真正想读的文件。code.php?src=code.php&mode=hl:用 highlighter 查看code.php自身。code.php?src=code.php&hl[0]=function&mode=hl:同样查看源码,但额外高亮function。
也就是说,题目没有隐藏源码。真正要看的就是 code.php
怎么处理 GET 参数。
核心代码先按 query string 里的参数顺序遍历 $_GET:
1 | foreach ($_GET as $key => $value) |
src 的白名单只允许三个文件:
1 | static $whitelist = array( |
目标是让 highlighter 读取白名单之外的 solution.php。
Solution
先把约束列出来:
- 直接访问
?src=solution.php&mode=hl会失败,因为src先被白名单检查改成false。 - highlighter 读文件前只做路径字符清理:去掉
/、\、..,但不会重新检查 whitelist。 - PHP 在脚本开始执行前已经把完整 query string 解析进
$_GET;后面的foreach ($_GET as ...)只是按参数插入顺序处理每个 key。
题目源码里甚至把方向提示写出来了:
1 | # if you like a hint: There is a main logical error in this script, |
这里的点不是 PHP 语法 trick,而是 code-flow mistake:检查和使用的顺序错了。
src 的检查函数失败时,只是修改
$_GET['src'],没有 return /
exit,也没有把验证后的文件名保存到一个独立变量:
1 | function php0816SetSourceFile($filename) |
真正读取文件的位置在 highlighter 里:
1 | function php0816Highlighter() |
对比两个请求就很清楚。
正常顺序会失败:
1 | ?src=solution.php&mode=hl |
处理流程:
src=solution.php先被处理。solution.php不在 whitelist 中,$_GET['src']被改成false。mode=hl后处理,highlighter 再读Common::getGet('src'),拿到的已经不是原始文件名。- 页面返回
File not Found。
把 mode 放到 src 前面:
1 | ?mode=hl&src=solution.php |
流程就反过来了:
- PHP 已经把完整 query string 解析进
$_GET,所以$_GET['src']此时已经存在,值是solution.php。 foreach第一个处理到mode=hl,调用php0816execute('hl')。- highlighter 立即执行
Common::getGet('src'),读到尚未被 whitelist 改写的原始值solution.php。 file_get_contents('solution.php')成功读取文件。- 后面才轮到
src=solution.php的 whitelist 检查,但文件已经被输出,已经太晚。
用 curl 验证坏顺序:
1 | $ curl -sL \ |
再验证利用顺序:
1 | $ curl -sL \ |
返回的 <pre> 里能看到
solution.php:
1 |
|
这类 bug 可以看作参数处理流程里的 TOCTOU:检查逻辑和使用逻辑都存在,但程序允许“使用”发生在“检查”之前。
修复方式不是再补一个字符串过滤,而是让“最终被使用的文件名”只能来自验证后的状态。例如:
- 先统一解析全部参数,完成 whitelist 检查后再执行
mode动作。 - 不把安全状态写回
$_GET,而是使用独立的$sourceFile变量保存验证后的文件名。 - 在
php0816Highlighter()内部对最终文件名重新做 whitelist 检查。