Skip to content

<!-- title: file-inclusion -->

文件包含漏洞详解:原理、利用与防御

一、漏洞原理

文件包含漏洞是指 Web 应用在引用文件时,允许用户控制"包含哪个文件",导致攻击者可以加载非预期文件甚至恶意代码的漏洞。它常见于 PHP 等支持动态包含语法的语言,按加载目标的来源分为两类:

  • LFI(Local File Inclusion,本地文件包含):能加载服务器本地的任意文件。配合上传功能或日志文件,可升级为代码执行,是最常见的形态
  • RFI(Remote File Inclusion,远程文件包含):能加载远程 URL 的文件(需 PHP 配置 allow_url_include=On,现代默认关闭),攻击者可直接让服务器执行自己服务器上的代码,危害最烈

漏洞根源依旧是"用户输入被当代码路径执行"这一信任问题:开发本意是让 page=index 映射到 pages/index.php,但拼接后没有校验,攻击者传入 page=../../../../etc/passwd 就能用 ../ 目录跳转序列读到系统任意文件——这也是文件包含与目录遍历常常成对出现的原因。

PHP 的四个包含语句 includeinclude_oncerequirerequire_once 在处理不存在或不可读文件时行为略有差异,但对漏洞本身没有本质影响;真正决定危害的是"路径可控"和"解析方式"。

二、示例代码

以下是一个经典的文件包含漏洞页面(虚构站点 example.com):

php
<?php
// index.php —— 通过 GET 参数决定加载哪个页面模块
$page = $_GET['page'];
include($page . ".php");   // 致命点:路径完全由用户控制
?>

正常访问 index.php?page=about 加载 about.php;攻击者访问 index.php?page=../../../../etc/passwd%00(旧版 PHP 的截断技巧)即可读出系统账户文件。修复后的安全写法是白名单映射:

php
<?php
// 安全版:用户只能选择白名单里的"逻辑名",路径由服务端决定
$allowed = [
    'home'  => 'pages/home.php',
    'about' => 'pages/about.php',
    'news'  => 'pages/news.php',
];
$page = $_GET['page'] ?? 'home';

if (!isset($allowed[$page])) {
    http_response_code(404);
    exit('页面不存在');
}
include($allowed[$page]);   // 路径不含任何用户输入
?>

三、三种典型 Payload(教学演示)

1. 目录跳转读系统文件

text
?page=../../../../etc/passwd
?page=....//....//....//etc/passwd     (过滤 ../ 时的双写绕过思路)

利用 ../ 逐层跳出当前目录直达系统根。Windows 服务器对应 ..\..\windows\win.ini。这类读取是 LFI 漏洞验证的第一步,也是应急响应中判断"是否发生过探测"的日志特征。

2. 读取并执行会话文件(LFI 升级为代码执行)

若攻击者能把一段 PHP 代码注入到服务器上任何"会被包含的文件"中——例如写入了 $_GET 数据的会话文件、Web 日志、或上传的"图片马"——再通过 LFI 包含它:

text
?page=../../../../tmp/sess_xxxxxxxxxxxxxxxx

被包含文件中的 PHP 代码将被执行。这条链路解释了为什么 LFI 从"读文件"升级为"执行命令"只差一个可写位置,防御必须整体考虑。

3. 伪协议读取源码(PHP filter)

PHP 独有的伪协议可以在不执行的情况下读出文件经过 Base64 编码后的内容

text
?page=php://filter/convert.base64-encode/resource=index

index.php 本身 Base64 编码后输出,攻击者解码即得源码。这条 Payload 的教学意义:它是无损读源码的标准手法,也是判断目标是否为 PHP 的指纹之一。

四、防御建议

  1. 白名单映射是根治:永远不要把用户输入拼进 include/require 路径;用"逻辑名 → 真实路径"的映射数组,路径字符串中不含任何用户输入
  2. 关闭远程包含:确认 php.iniallow_url_include=Offallow_url_fopen 按需最小化,RFI 即从根源消失
  3. 固定前缀并校验真实路径:无法完全白名单时,强制拼上固定目录前缀,再用 realpath() 校验解析后的路径必须落在该目录内,杜绝 ../ 跳转
  4. 关闭危险伪协议:通过 php:// 过滤器限制或使用 open_basedir 划定 PHP 可访问目录,封堵 php://filterdata:// 等协议滥用
  5. 最小化可写位置:会话、日志、上传目录与 Web 包含路径隔离,避免"LFI + 可写文件 = 代码执行"的组合拳成立
  6. 升级语言栈:现代框架(路由 + 模板引擎)天然不提供"参数决定包含文件"的写法,老式 PHP 站点改造是最彻底的修复

五、总结

文件包含漏洞的攻防围绕一个核心展开:"包含谁"这个决定权必须留在服务端。白名单映射写法只需五行代码就能根治,却仍有大量遗留系统在裸奔。对学习者而言,LFI/RFI 是理解"路径即权限"的最佳案例——它能与文件上传、会话机制、日志系统组合成完整的攻击链,也是防御方案必须做纵深设计的原因。

仅用于学习交流的防守型安全知识库