资讯动态

文件包含漏洞实战:从LFI到RFI再到日志注入与图片马组合利用

发布时间:2026/10/19 17:35:19 来源:尧图企业网站定制
文件包含漏洞这种问题刚接触 Web 安全时我总觉得它很低级无非就是 include 了一个用户传进来的参数。直到在一套开源靶场里把文件包含模块从低到高全刷了一遍才发现里面的细节比想象中多很多。本地包含、远程包含、伪协议读源码、日志注入、图片马配合上传每一样都够喝一壶。这篇文章把整个实战过程拆开来讲连踩坑记录一起放出来适合刚接触 Web 安全的读者也适合已经会用工具但没理清原理的人。1. 文件包含漏洞到底是怎么来的1.1 从 include 继承来的信任问题先看 PHP 里的 include 机制。include 和 require 的作用是在运行时把另一个文件的内容加载进来并且文件里的 PHP 代码会在当前脚本上下文里执行。这个设计本身没有错错的是开发者把用户可控的参数直接拼到了 include 的路径上比如写一行include($_GET[page]);。用户传什么include 就加载什么文件包含漏洞就这么出现了。打个比方include 很像厨房里的预制菜你把一个菜包拆开直接倒进锅里本来这个菜包应该由供应链固定提供结果现在允许顾客自己指定菜包那顾客就可能拿一个写着“随便打开看看”的神秘包裹。攻击者用这个能力可以读取任意文件更危险的是一旦包含了一个包含 PHP 代码的文件这些代码就会在应用上下文里被直接执行。include 和 require 还有一点差异需要记住include 找不到文件时只给警告脚本继续往下跑require 找不到文件会直接中断页面上就是一片空白。很多新手看到白屏会以为是漏洞没触发其实可能是 require 把整个流程掐断了。这一点先放脑子里后面排查问题时会用上。1.2 本地包含、远程包含、伪协议文件包含漏洞通常分成三类我习惯用一个表把它们摆在一起看类别含义常见利用方向关键条件本地文件包含LFI包含目标机器上的本地文件路径穿越读取系统文件配合日志或上传执行代码参数可控危险函数不校验远程文件包含RFI包含来自远程 URL 的脚本把恶意代码放在可控 HTTP 服务上再让目标包含allow_url_includeOn伪协议PHP 自带的流封装器php://filter读取源码php://input传入代码PHP 版本支持filter 可用远程包含的危害通常比本地包含更大因为攻击者不需要在目标机器上找可写文件只要目标能访问到自己的服务器就行。不过 PHP 很多版本默认关闭allow_url_include所以 RFI 在现实里的出现率比 LFI 低但依然值得掌握。php://filter最常见的写法是php://filter/readconvert.base64-encode/resourceindex.php。它会先把 index.php 的内容做一次 base64 编码再输出这样 PHP 代码不会被解析执行你可以拿到原始源码。一句话理解普通 include 会把 PHP 文件“煮熟”给你看结果filter 则是把原材料打包成罐头再交给你不开火也能看到里面装了什么。2. 实战前准备环境、等级、工具2.1 搭一套能反复折腾的靶场我建议用本地虚拟机或者容器来部署这套开源靶场千万不要拿生产环境或者未授权的站点练手。安装过程本身不复杂装好 PHP 环境、配置数据库、把靶场代码放到 Web 目录、走一遍初始化流程。但有几个细节值得注意。首先是 PHP 版本兼容性。太新或太旧的版本可能导致模块页面报错我遇到过某个版本下文件包含模块的页面能打开但提交参数后直接白屏最后发现是 PHP 版本不兼容。其次是open_basedir配置。如果这个限制被打开PHP 就只能访问指定目录内的文件路径穿越基本会失败靶场默认一般关闭但你自己部署时可能不小心从某个配置模板里带出来了。另外一定要养成用快照的习惯。我每次切换安全等级之前都会恢复一次快照因为上一轮测试可能已经上传了文件、改写了日志不还原会影响下一轮的判断。快照还能让你随时回退到干净环境尤其是测试日志注入时日志文件会被写得很乱快点恢复比手动清理效率高得多。2.2 低中高三个等级到底模拟了什么这套靶场的安全等级不是随便设计的它对应了三种常见的开发习惯完全不过滤、简单黑名单过滤、白名单限制。低等级就是传什么参数就直接包含最适合理解漏洞原始形态中等级会过滤../、http://这类关键字但过滤方式非常粗糙高等级限制参数必须以某个固定前缀开头逼着你寻找其他辅助漏洞。切换等级后payload 基本要重新构造同时上一次测试产生的 session 也可能影响结果。我自己的习惯是切完等级先重置一下会话再用最基础的绝对路径试一次确认当前等级的真实过滤状态。2.3 命令行才是好帮手很多初学者习惯直接在浏览器地址栏里改参数这没问题但浏览器会自动编码、自动缓存构造复杂 payload 时很不方便。我更喜欢用命令行 HTTP 客户端做精细控制比如curl -i http://127.0.0.1/app/vuln.php?pageindex.php-i看响应头-s静默模式省去进度条-o保存响应。配合 grep 可以快速判断包含是否成功curl -s http://127.0.0.1/app/vuln.php?page/etc/passwd | grep root:如果看到root:x说明包含成功。测试路径穿越深度时与其一层一层手试不如跑一个小循环for i in $(seq 1 8); do prefix$(python3 -c print(../ * $i)) resp$(curl -s http://127.0.0.1/app/vuln.php?page${prefix}etc/passwd) if echo $resp | grep -q root:; then echo depth$i; fi done抓到这样的输出你就能直接确认需要多少个../。抓包工具我也会用但一定是先理解了参数再上工具不然面对一堆请求只会更加困惑。3. 低安全等级先把基础漏洞打透3.1 用绝对路径做一次本地包含低等级下第一步我会直接试绝对路径把 page 参数改成/etc/passwd原始pageindex.php 测试page/etc/passwd如果页面里出现了root:x:0:0:root:/root:/bin/bash之类的内容漏洞就算确认了。这种方式不需要关心当前目录因为 include 可以直接加载绝对路径。不过很多环境会强制拼接前缀比如代码写成include pages/ . $_GET[page];这时绝对路径会被拼成pages//etc/passwd不一定有效。所以相对路径穿越也要练不能只会一种姿势。3.2 相对路径穿越数清楚要跨几级相对路径穿越的核心是数清楚目录层级。假设应用入口在/var/www/html/pages/index.php想读/etc/passwd就要从pages目录一路../回到根目录。每一级../代表向上一级构造出来的结果就是../index.php 当前位置上一级的 index.php ../../etc/passwd 上两级后 etc/passwd ../../../etc/passwd 上三级后 etc/passwd实际操作时如果不知道深度我建议先用../index.php看有没有回显再逐步增加层级。不要一次到位因为有时候目标文件存在但路径差一级页面会安静得让你以为漏洞不存在。这里还要提个细节在 Linux 环境下路径分隔符是斜杠URL 里直接写/就好在 Windows 环境下则要尝试反斜杠..\..\etc\passwd。靶场大多跑在 Linux 上但你分析真实环境代码时要先确定系统类型再决定用哪种穿越字符串。3.3 php://filter 读取 PHP 源码直接包含index.php时页面只会显示执行结果看不到一行源代码。这时候用 filterpagephp://filter/readconvert.base64-encode/resourceindex.php返回的是一串 base64复制出来用base64 -d解码就能看到源码。这一步在实战靶场里几乎必用因为源代码是一切绕过思路的依据你会看到后台对参数做了什么替换、用了哪个函数、有没有白名单。我拿到源码后会先搜几个关键词include、require、$_GET、$_POST、str_replace、file_exists基本就能定位漏洞点。filter 不受allow_url_include影响它走的是本地流封装不是远程 URL所以即使远程包含被禁本地读取依然有效。只有当open_basedir限制或者代码里主动过滤了php://时它才会失效。3.4 远程包含与日志注入的验证低等级下如果allow_url_includeOn可以验证远程包含。演示时不要连外网直接在目标机或者本机起一个临时 HTTP 服务放一个无危害的测试文件echo ?php phpinfo(); ? /tmp/test.txt python3 -m http.server 8000 --directory /tmp然后构造pagehttp://127.0.0.1:8000/test.txt如果页面出现 phpinfo 或执行了代码说明 RFI 可用。这里必须再强调一次这类验证只能在自己可控的靶场环境里做。如果返回failed to open stream第一反应应该检查allow_url_include是不是 Off而不是盲目换 payload。日志注入的思路也很有意思。Web 日志会记录请求头而请求头里的 User-Agent 等内容经常是可以由我们控制的。那就把一段无害的 PHP 代码写进日志再让文件包含去加载日志文件curl -A ?php phpinfo(); ? http://127.0.0.1/app/index.php然后包含日志路径常见的路径有/var/log/apache2/access.log、/var/log/nginx/access.log、/opt/lampp/logs/access_log。如果页面出现 phpinfo说明日志注入成功。但要注意 Web 进程如果没有权限读日志这一步会失败这也正是权限最小化配置的意义所在。4. 中安全等级黑名单过滤不是终点4.1 先摸清过滤到底做了什么中等级再传../会发现路径穿越失效但这时候不要慌。先用php://filter读源码或者根据返回行为去推断过滤规则。如果能看源码大概率会看到类似这样的逻辑把../和..\\替换成空把http://和https://也替换成空。这种“替换一次”的黑名单最大的问题是没有做规范化处理也没有递归检查。攻击者只要在关键字周围预留足够的字符让删除动作本身产生新的关键字绕过就出现了。如果不能看源码可以通过行为推断传一个....//etc/passwd看看返回什么如果成功基本就能确定是简单字符串替换。4.2 双写绕过的一个经典示例用....//绕过../过滤原理其实不复杂。过滤规则会查找字符串../并删除第一次匹配到的位置。你构造....//etc/passwd在....//这段字符里第二到第四个字符正好是../删除后剩下的字符会重新拼成../../etc/passwd。换句话说过滤把里面的一层../拿掉外面又长出一层../等于没过滤。同样的思路也能用在协议上。如果后端过滤http://那就构造hthttp://tp://127.0.0.1:8000/test.txt删除中间的http://后剩下的恰好是http://127.0.0.1:8000/test.txt。这种绕过成立的核心原因是服务端只替换了一次且没有递归检查很多防御代码以为删掉关键字就安全了却忽略了删除动作本身会制造新的关键字。4.3 编码绕过要看服务端处理顺序除了双写URL 编码和二次编码也是常用思路。如果服务端先做了一次 URL 解码再对解码后的字符串做过滤那么你传%2e%2e%2f解码后是../会被过滤掉。但你传%252e%252e%252f服务端解码一次后变成%2e%2e%2f过滤时看到的仍然以%开头等真正做文件操作时再解码一次就绕过了一层检查。不过这个技巧不是在所有环境里都灵取决于 PHP 版本和应用内部的处理顺序。我最想提醒的是编码绕过不要盲打。盲打等于碰运气先读源码确认过滤发生在解码的哪一步你才知道该用单层编码还是双层编码。4.4 中等级下还能不能 RFI中等级如果已经过滤http://你可以用大小写绕过比如HTTP://或者用上一节的双写协议方式。但话说回来RFI 必须依赖allow_url_includeOn如果这个开关本来就没打开协议过滤绕得再漂亮也没用。实际操作中中等级我倾向于优先做 LFI。先读源码找到可写文件位置再通过日志包含或者上传配合达到代码执行效果。毕竟远程包含依赖服务端配置本地包含依赖的是应用自身缺陷后者往往更可控也更能在真实场景里复用。5. 高安全等级没有万能钥匙只有组合拳5.1 白名单校验让直接穿越失效高等级把校验升级成了白名单要求参数必须以某个固定前缀开头比如file。这一下子堵住了很多路相对路径穿越不行因为参数会被限制成file://开头php://不行http://不行普通绝对路径也不行。你能做的只剩下file:///etc/passwd这类合法前缀内的协议。但白名单并不是终点。它只是让单个文件包含漏洞能直接做的事变窄了并不代表整个系统安全了。如果应用同时存在文件上传功能或者日志文件可以被我们写入还是可以组合出可执行的代码。5.2 图片马和上传功能配合当文件上传功能存在时组合利用是常见路径。思路是上传一个看起来是图片的文件但里面藏着用于验证的 PHP 代码然后让文件包含功能去加载这个文件的绝对路径。include 不会在意扩展名只要加载的内容里有?php ?标签就会按 PHP 解析执行。我常用的无危害验证文件是这样生成的echo GIF89a test.gif echo ?php phpinfo(); ? test.gif文件头加上GIF89a能过掉简单的文件类型校验。上传成功后把返回的路径填到pagefile:///绝对路径/test.gif。如果页面执行了 phpinfo就说明组合利用成功。这里有个细节值得展开上传目录如果被 Web 服务器禁止执行 PHP比如 Nginx 下常见的location ~ \.php$规则不会把.gif交给 PHP 解析但 include 是应用层主动调用不走 Web 服务器的文件关联所以依然会执行。这就是为什么文件包含漏洞和上传漏洞组合起来特别危险也解释了为什么很多安全基线要求上传目录不能有执行权限。5.3 日志包含在高等级下的尝试如果上传功能不可用日志包含是备用选项。构造方式不变在 User-Agent 里写代码然后让pagefile:///var/log/apache2/access.log只要白名单允许file://协议这条路就还有机会。但实际成功率取决于日志格式、路径是否可猜、日志文件是否过大以及 Web 进程是否有读取权限。我第一次尝试时日志文件有几千行返回内容巨大页面卡了好一会儿。正确做法是先确认路径正确然后用curl -s把响应保存到本地文件再在本地搜索特征字符串不要直接在浏览器里硬等。如果日志文件过大包含它本身就会成为一个性能问题这也是为什么很多防护方案里会定期清理日志。5.4 不同等级利用难度对比安全等级直接读文件读 PHP 源码远程包含组合执行代码总体评价低轻松轻松配置开启时轻松简单适合入门中双写后轻松双写后可用协议绕过可用需要配合理解黑名单缺陷高只能file://基本失效不可用需要上传或日志配合思路大于技巧这张表是我刷完整个模块之后整理的能帮你快速判断遇到不同场景时优先尝试哪个方向。真实环境里没人会告诉你现在是第几级你需要通过回显和源码判断过滤强度再决定组合路线。6. 常见问题与排查技巧实录6.1 包含后一片空白常见原因有三个包含的文件不是 PHP、文件不存在、PHP 错误被隐藏。如果你用的是require包含一个不存在的文件时脚本会直接终止页面什么都不显示这最容易让人误判。排查顺序我一般是这样先打开 PHP 的display_errors把报错露出来再换成绝对路径测试排除路径深度问题最后用php://filter看目标文件到底能不能被读取。记住一个原则不是所有空白都是漏洞失败也可能是判断依据没选对。6.2 路径穿越一直无效先检查是不是过滤了../可以构造....//绕过再试。再检查浏览器有没有把../自动编码成%2e%2e%2f如果后端没做解码请求路径就是错误的。最后确认当前目录深度是否和猜的一致用循环脚本从 1 到 8 层都试一遍同时用 grep 判断不要靠肉眼盯大段回显。还有一个容易忽略的点如果应用入口在一个很深的目录里你只写一两个../根本出不了 Web 根目录。这时候要用绝对路径或不断向上跳直到能跨到根目录。找深度最可靠的方法就是循环测试比手猜准得多。6.3 php://filter 没输出如果过滤规则直接拦了php://返回会是错误字符串。如果页面空白可能是 filter 写法不对。有些 PHP 版本要求readconvert.base64-encode有些版本直接用convert.base64-encode也能跑但 resource 后面必须跟真实路径。我遇到过一种情况资源路径写对了但包含时前面又被拼接了一个目录前缀导致实际加载的文件不对。排查时可以先用本地命令验证过滤器名字本身有没有问题再到靶场里看是否存在路径拼接。6.4 远程包含失败远程包含失败的第一检查项永远是allow_url_include。这个配置是 Off 时include 远程 URL 会被直接拒绝和协议过滤没关系。其次检查allow_url_fopen它控制的是文件打开类函数不能替代前者。还有网络连通性。目标机如果访问不到你放的测试文件地址RFI 自然失败。我的一次测试里误用了本机回环地址目标机根本访问不到排查半天才发现是地址选错了。自己搭的 HTTP 服务地址一定要选目标机能访问到的。6.5 防护建议从根上拔掉包含漏洞代码层面尽量不要把用户参数直接拼进 include 或 require。如果必须动态加载文件用一个枚举白名单映射比如pageabout对应about.php用户传什么都不能直接当文件名。服务端配置上把allow_url_include关掉设置目录白名单用realpath()校验最终路径必须在允许的目录内同时避免 Web 进程权限过大。日志目录和上传目录要单独配置不允许执行 PHP。这些措施单独看都简单但叠加起来效果明显就算某个点没堵住下一层依然能兜底。6.6 我的一点实战心得刷完整个靶场之后我对文件包含漏洞的判断顺序基本固定了先确认能不能读系统文件再尝试读源码最后才考虑组合姿势。很多人一上来就想着执行代码反而忽略了漏洞本身能暴露多少信息。其实读源码往往比直接 getshell 更有价值因为它能把整个应用的过滤逻辑和业务逻辑全部摊开。还有个小习惯每次切换安全等级前恢复快照免得上一轮的日志或上传文件影响下一轮测试结果。这套流程用熟了你遇到真实场景时至少知道第一步该往哪儿看而不是拿着 payload 到处乱试。文件包含漏洞技术点不深但坑不少亲手把每个等级都过一遍比看十篇文章都有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑