文件包含漏洞有个不太好听、但很准确的外号:它不像注入那样“改语句”,更像是应用主动说——“你告诉我读哪个文件,我帮你加载进当前执行上下文”。在 PHP 的老项目里,这句“帮你加载”一旦碰上用户可控路径,轻则任意文件读取,重则代码执行。国内安全圈常把它和LFI(本地文件包含)、RFI(远程文件包含)、日志投毒、伪协议放在同一条故事线里讲。
另外先泼冷水:现代 PHP 默认往往关掉了远程包含,框架也很少再写裸include $_GET['page']。但存量业务、插件、模板主题、错误的下载/预览接口里,这类洞仍在。学会它,更多是学会**“用户输入绝不进入文件包含/加载 API”**这条铁律。
一、文件包含是什么病
以 PHP 为例(其它语言也有“动态加载模块/模板”的近亲问题):
include ($_GET['file'] . '.php'); require、include_once、require_once 同理本意可能是:?file=about加载about.php。
一旦file可控,攻击者就可能改成路径穿越,去包含系统文件,或在配置不当时包含远程资源。
关键点在于:被包含的文件内容,会按 PHP 代码执行上下文处理(若其中出现可执行的 PHP 代码)。因此:
- 包含
/etc/passwd这类纯文本 → 常常是读取(源码/配置泄露); - 包含带有
<?php ... ?>的内容 → 可能执行; - 若攻击者能控制“被包含内容里出现 PHP 代码”,就接近 GetShell。
这就是为什么上传漏洞、日志写入口、会话文件、缓存文件,常和 LFI 绑在一起。
二、LFI 与 RFI:别混着背
| LFI 本地文件包含 | RFI 远程文件包含 | |
|---|---|---|
| 目标 | 服务器本地文件 | 远程 URL 上的资源 |
| 典型条件 | 路径可控 + 包含函数 | 额外需要allow_url_include一类危险配置为开 |
| 现代默认 | 仍常见于老代码 | 相对少,因默认常关闭远程包含 |
| 危害 | 读敏感文件、组合执行 | 直接拉远程脚本执行,往往更直接 |
人话:
- LFI:打的是“读谁、包谁”;
- RFI:打的是“远程下载并执行”(配置允许时)。
测试时先看 PHP 配置与代码,别一上来假设 RFI 可行。很多报告把普通路径穿越读文件也写成 RFI,这不严谨。
三、LFI 的常见长相
1. 页面路由式
index.php?page=help→include $page . '.php'
攻击者尝试page=../../../../etc/passwd(是否截断.php后缀、是否有过滤,决定手法)。
2. 模板 / 语言包 / 主题
lang=en→ 加载languages/en.php
看起来无害,一旦拼接不严谨,一样穿越。
3. 下载与预览“读文件”接口
有的不是include,而是readfile/file_get_contents然后输出。那是任意文件读取,和包含近亲。若再把内容喂给危险函数,可能升级。分类时写准确:读取 ≠ 包含执行。
4. 框架里的意外
某些错误的视图加载、插件机制、调试开关,也会引入动态包含。审计时搜:include、require、include_once、file_get_contents、readfile、fopen与用户输入的交集。
四、从“读文件”到“执行”:攻击者缺的那一块
纯 LFI 经常只能读到配置、密钥、源码——已经很严重。若还想执行代码,攻击者需要让“被包含的本地文件”里出现可执行脚本内容。常见思路(概念级)包括:
- 文件上传:先把带脚本内容的文件传到服务器某处,再 LFI 包含它(扩展名不一定是
.php,取决于包含方式); - 日志投毒:让访问日志/错误日志里记下一段脚本特征,再包含日志文件;
- 其它写入点:会话文件、缓存、邮件スプール、图片处理残留——凡是攻击者能写入可控内容的路径;
- 伪协议:不一定写文件,而用 PHP 封装协议在包含/读取时做转换或借道(见后文)。
防守方要切断的,不只是“禁止../”,而是:用户输入永不进包含函数;写入点可控内容不可被包含执行;上传目录不可执行且路径不可预测。
五、日志投毒:把日志变成“临时源码”
1. 原理用故事说
Web 服务器通常把请求行、User-Agent、Referer 等写入访问日志。若应用存在 LFI,且攻击者能定位到日志路径,就可能:
发送一个请求,让日志里留下可控字符串(甚至 PHP 代码形态) → 再用包含漏洞加载该日志文件 → 日志中的代码被 PHP 解释执行这叫日志投毒(Log Poisoning),是 LFI 经典组合技。它不神秘,条件却很硬:
- 要有可用的本地包含;
- 要猜对或读到日志路径;
- 日志内容要真能进 PHP 引擎并被当成代码;
- 现代环境可能日志不可读、路径隔离、PHP 不解析该路径、或禁用了危险函数。
2. 授权测试时怎么理解“证明”
在靶场里,教学演示会展示“UA 里写入标记 → 包含日志 → 标记被执行/被包含显示”。
在真实授权项目里,我更建议:
- 先证明可包含已知文件(如公开的无害文件或配置中允许的路径);
- 再评估日志是否可读、是否在 web 用户权限下;
- 慎用真实恶意载荷;用无害唯一探针证明“可控输入进入了被包含文件”往往足够写进报告;
- 生产环境优先推动修复,而不是把日志变成 shell。
3. 防守:专克日志投毒
- 根治:消除 LFI(参数化路由白名单,禁止用户拼路径);
- Web 用户对日志目录不可读(权限分离);
- 日志放在 webroot 外;
- 限制 PHP open_basedir;
- 容器场景下只读文件系统与最小挂载;
- 监控异常包含路径、异常
../访问(应用日志与 WAF)。
不要幻想“过滤 User-Agent 里的 php 关键字”能当主防线——绕过面太大,且日志源不止 UA。
六、PHP 伪协议:包含与读取的“另一扇门”
PHP 提供多种封装协议(wrappers),可在file_get_contents、include、require等场景使用。对开发很方便,对安全很危险——因为用户若能控制路径字符串,就可能喂进伪协议。
下面按安全含义介绍常见协议,便于审计与防护;具体行为与 PHP 版本、配置(如allow_url_fopen)有关。
1.php://filter——读源码的常客
常被用来在包含/读取时做过滤转换。安全讨论里最著名的用途是:把源码以某种编码方式读出,便于在页面回显受限时仍证明任意文件读取(例如读配置文件、读业务源码找密钥)。
对防守方的含义:
- 即便没有“执行”,读到数据库密码、云密钥、会话配置也足够致命;
- 过滤
../却没禁伪协议,等于拦了穿越、没拦 wrapper; - 修复必须是白名单,而不是黑名单拦
php://。
授权测试报告里写清:通过伪协议读取了哪些文件的哪类敏感信息(注意脱敏展示)。
2.php://input——请求体当文件
在特定配置与用法下,请求体可被当成流读取。历史上与包含组合时风险很高。现代默认配置与写法下要具体分析;审计见到用户可控进include且可能触及 input 流,应按高危看。
3.data://等数据包装
在允许时,可能把数据直接放进“路径”里。依赖allow_url_include等配置,现代多默认关闭。审计仍要看配置落地,别只看代码。
4.phar://、zip://、phar反序列化相关
phar://等可把归档当文件系统访问。安全上它还与Phar 反序列化等问题有过大量讨论:某些文件操作碰 Phar 元数据时触发反序列化。这已超出“纯包含”范畴,但同属“路径可控 + 封装协议”家族。
防守:升级 PHP;限制危险文件操作的用户输入;不信任用户上传的 phar/zip 进入敏感文件函数。
5.file://与其它
明确指定本地文件。有时用于绕过只拦了相对路径的弱过滤。白名单面前同样应失效。
6. 伪协议防御总纲
对“文件路径”类参数:
只允许枚举值(about/help/contact)映射到内部路径 禁止用户字符串直接进入 include / file_get_contents / fopen 关闭不必要的 allow_url_include 收紧 open_basedir 上传文件不进可包含路径黑名单拦php://、phar://、zip://可以当辅,不能当主。
七、RFI:为什么现在讲得少,但仍要会查
远程文件包含依赖危险配置(如allow_url_include=On)以及代码把 URL 丢进包含函数。若条件成立,攻击者可让服务器拉取远程脚本并执行——链路短、危害大。
运维检查:
php.ini 中 allow_url_include allow_url_fopen(影响面不同,但同属 URL 当文件相关配置)代码检查:是否有include $_GET[...]且未白名单。
发现 RFI 条件,优先级按紧急漏洞处理:先关配置、再改代码。
八、和上传、解析、会话的组合
回顾本系列上传篇:上传不一定直接 GetShell,但可为 LFI 提供“可包含文件”。
会话文件若可预测路径且含用户可控数据,也可能成为包含目标(视存储方式而定)。
日志投毒则是“不经过上传功能”的写入。
画链路时建议:
写入点(上传/日志/缓存) + 包含点(LFI) + 执行条件(PHP 解析)缺任何一环,危害可能降级为“仅读取”或“不可用”。修复任意一环都有价值,修包含点是根。
九、授权测试方法(务实版)
1. 发现
- 参数名像
page、file、path、template、lang、doc; - 报错泄露
include/require路径; - 白盒搜包含函数与用户输入。
2. 确认 LFI
- 尝试包含公开可知文件或应用自身静态文件;
- 观察回显差异、报错、时间(慎用暴力);
- 证明路径穿越或伪协议读取成功。
3. 评估升级面
- 是否可读配置与密钥;
- 是否存在可控写入点;
- 日志是否可读;
- 配置是否允许远程包含。
4. 报告
分类写:任意文件读取 / 本地文件包含 / 可打到代码执行的条件。
修复给白名单示例与配置项。
避免在报告附件里留真实后门。
十、代码与配置层修复
1. 白名单映射(推荐)
用户传 page=about → 仅当 about 在允许集合 → 内部映射到 /views/about.php → include 固定内部路径用户字符串永不拼接进文件系统路径。
2. 禁止动态包含用户输入
能改路由就改路由;遗留代码用映射表兜住。
3. 配置加固
allow_url_include = Off- 合理
open_basedir - 日志与 web 用户权限分离
- 关闭不必要的危险函数(按业务评估)
4. 显示与错误
生产关闭详细错误路径回显,减少“帮攻击者找包含位置与日志路径”。
5. 检测
WAF/应用层对../、php://、phar://可告警;
RASP/HIDS 对异常文件读取、异常 include 路径做行为检测;
仍须源码修复。
十一、开发评审口头禅
- 这个
include/require/file_get_contents的路径里有用户输入吗? - 是白名单映射还是字符串拼接?
allow_url_include是否关闭?- 上传/日志/缓存是否可能被同一进程包含?
- 报错会不会泄露绝对路径?
十二、收尾
文件包含漏洞的核心,不是伪协议名单背得多全,而是一句话:
文件系统路径与包含/读取 API,不能直接吞用户输入;
用户若还能控制“被加载内容”,读取就会升级成执行。
日志投毒教你重视写入点与权限分离;
PHP 伪协议教你重视封装协议与任意文件读取;
RFI 教你重视危险配置开关。
今晚若只做一件事:在代码库里全局搜索include/require/file_get_contents,对每一个命中问“路径从哪来”。
问完,你对 LFI/RFI 的防守,就已经比只收藏伪协议列表的人更接近实战。