☰
深入剖析文件包含漏洞:原理、利用链与安全防护实践
2026/10/19 6:02:48 网站建设 项目流程

文件包含漏洞对我来说是个熟悉得不能再熟悉的东西。早些年做安全评估时,遇到过不少站点就是因为这类问题被撕开口子,有的甚至从读取一个配置文件开始,一路摸到服务器权限。这个漏洞看起来简单,危害却一点都不小,而且它的变种极多,从脚本语言的内置机制到中间件的配置差异,每一层都可能藏着新的利用面。

这篇内容我打算把文件包含从原理、利用、检测到修复整条链路拆开来讲。你会看到它为什么会出现、攻击者通常怎么下手、我在审计时重点关注哪些地方,以及修复时哪些方案是真的管用。不管你是刚入门的开发,还是已经有几年经验的测试人员,照着这个思路捋一遍,应该都能对文件包含建立一套比较完整的判断框架。

1. 文件包含漏洞的本质与常见成因

先说点基础的。文件包含这个动作本身是编程语言提供的正常功能,目的是把一段公共代码引入到当前文件中复用。最常见的是PHP的include、include_once、require、require_once,Java里的include指令,ASP.NET里的Response.WriteFile,都是类似机制。问题是,这个功能一旦被外部输入影响了文件的路径,攻击者就有机会让程序去加载他自己想要的文件,漏洞就出现了。

1.1 文件包含究竟在做什么

PHP里写include($page)的时候,程序会按照$page这个变量的值去读取文件,然后把文件内容当作PHP代码执行。正常情况下,这个$page应该是开发者写死的,例如include "./inc/header.php"。但很多业务场景需要动态切换页面,比如通过URL参数控制加载哪个模板:

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

这种代码在早期的个人站、CMS里非常常见。问题也正好出在这里——$_GET['page']是用户可以直接控制的,如果没人校验它的内容,攻击者传入一个路径,程序就会乖乖地去读那个路径。

这里需要注意一个关键区别:include、require这类语句不止是读取文件内容,而是会把文件内容当PHP代码解析执行。也就是说,如果攻击者能包含一个PHP文件,他等于在目标服务器上执行了任意代码。相比单纯读取文件内容的函数(比如file_get_contents),危险等级完全是两个层次。

1.2 本地文件包含与远程文件包含

行业内习惯把文件包含分成两类,本地文件包含和远程文件包含。这个名字起的有点误导,好像区别只是文件在那台机器上,实际上它们的利用难度和危害范围并不一样。

本地文件包含,攻击者只能去包含目标服务器上已经存在的文件。比如包含/etc/passwd、/proc/self/environ这类敏感文件,或者通过包含日志文件、临时文件来实现代码执行。这种漏洞能不能利用成功、能读到什么,很大程度上取决于服务器上有什么文件,以及文件内容里有没有可控的PHP代码片段。

远程文件包含则更直接,攻击者可以在自己搭的服务器上放一个恶意脚本,然后让目标程序通过URL去加载执行。条件是目标环境开启了远程文件包含的功能,比如PHP的allow_url_include参数处于开启状态。现在PHP默认是关闭的,所以远程文件包含出现的频率比早期低了不少,但老系统、二次开发框架里仍然可能遇得到。

在危害等级上,远程文件包含一旦成立,基本等同于直接拿到一个命令执行入口,本地文件包含虽然通常需要结合其他条件,但只要攻击者读到了预期之外的文件,也已经是实打实的漏洞了。我在漏洞报告里看到很多忽略本地文件包含的案例,其实这是个误区,本地文件包含配合日志注入、会话文件利用,拿到shell的情况真不少。

1.3 开发者为什么总是写出这类漏洞

我见过太多文件包含漏洞的代码,总结下来就是三类原因。

第一类是图省事。动态页面要根据参数加载不同模板,开发者图省事直接把用户参数拼到include里,不加任何校验。这类代码常见于小站、外包项目、临时后台。

第二类是过滤不完全。有些开发者知道要防,写了过滤函数,但过滤规则有漏洞。比如只过滤了../,没过滤绝对路径;或者只替换了一次,攻击者用....//这种双写就能绕过;还有的过滤了http://,但攻击者用php://filter协议绕过去了。

第三类是框架和中间件配置不当。文件包含不一定只出现在业务代码里,有的CMS的模板引擎支持通过配置文件指定模板路径,攻击者如果改写了配置文件,同样能控制include的目标。还有一些伪静态规则、目录遍历与include的耦合,也会形成变种的漏洞。

这三种成因对应了三种不同的修复方向,后面我展开讲。

2. 危害分析:从读取文件到代码执行

文件包含漏洞最迷惑人的地方就是它的起点看上去不痛不痒——只是读一个文件而已。很多开发者觉得读到几个系统文件似乎不影响大局。这个想法特别危险,因为本地文件包含读取文件只是第一步,利用链条往下走,结果常常是主机沦陷。

2.1 一条完整的本地文件包含利用链

拿一个真实场景来走一遍。假设目标站点有个功能是选择语言,URL长这样:

http://target.com/index.php?lang=cn

服务端代码是:

include("lang/" . $_GET['lang'] . ".php");

看起来好像问题不大,lang目录下都是语言包文件。但攻击者可以做这么几步操作。

第一步,先确认存在文件包含缺陷。把lang的值改成../../../../etc/passwd,注意路径要穿越回根目录。层数取决于当前文件所在目录深度,猜不准就多试几层。如果返回了root开头的用户列表,说明读文件成功。

第二步,寻找一个内容可控的文件来间接执行代码。Apache和Nginx的访问日志是经典选择。攻击者往URL里塞一段PHP代码,比如:

GET /index.php?lang=<?php phpinfo();?>

这段代码会原样出现在访问日志里。然后在index.php中使用:

?lang=../../../../var/log/apache2/access.log

如果日志文件能被包含进来,里面的PHP代码就会被解析执行。Apache日志通常有空格和引号,直接执行容易报错,所以攻击者会用日志投毒和编码技巧让代码更干净。Nginx日志同理。

第三步,如果日志路径读不到或者日志格式限制太多,攻击者会尝试其他口子。比如session文件,PHP默认把session以文件形式存在/tmp目录,文件名通常是sess_加上会话ID,文件内容包含session里的变量值。如果业务逻辑里有写session的入口,攻击者往session里写入PHP代码,再包含session文件,一样能执行。

这条利用链告诉我两件事:本地文件包含一旦配合“可控内容写入文件”的入口,危害就完全不一样了;同时,临时目录、日志目录这些不显眼的地方,往往是链条闭合的关键。

2.2 伪协议与封装器:扩大攻击面的推手

前面提到过,include会把内容当PHP代码执行,这本身已经很危险了。但PHP还提供了伪协议,让include可以操作的范围远超普通文件系统。

php://filter是非常出名的一个。它可以在读取文件时对内容做转换,最常见的用法是base64编码。为什么要用base64?因为PHP文件本身是PHP代码,直接include会导致代码执行而不是内容显示,攻击者看不到原始代码。用php://filter/read=convert.base64-encode/resource=config.php,就能拿到config.php内容的base64编码,解码后得到源码。这是获取源码和数据库密码的经典手法。

php://input和data://则更暴力。data://text/plain;base64,后面直接跟一段base64编码的PHP代码,php://input读取请求体内容,攻击者POST一段PHP代码过去,include直接执行。这类利用在远程文件包含被限制之后变得非常常见,因为它们不需要服务器开启allow_url_include,只需要文件包含漏洞存在,就能直接到达代码执行。

除了这两种,还有phar://反序列化攻击链。phar文件内嵌一个序列化对象,配合文件操作函数在某些版本下可以触发反序列化,进而形成更复杂的攻击链。文件包含漏洞有时候只是链子的入口,后面接着的是反序列化、SQL注入、命令执行等各种后续利用。

2.3 截断、编码与绕过思路

很多人以为文件包含漏洞很好防御,加个.php后缀不就行了。实际操作没这么简单。

一种经典绕过方式是空字节截断,早期PHP版本里,URL中的%00可以截断字符串,include("../../../../etc/passwd%00.php")会被当作include("../../../../etc/passwd")处理。现在主流版本已经修复了这个特性,但老环境里仍然能用。

另一种是路径长度截断。Windows路径长度限制约260字符,Linux约4096。通过不断叠加./让路径超长,系统在解析时会截断尾部,把.php后缀丢弃。这个技巧在现代环境里也受限,但在特定文件系统下依然有效。

还有利用操作系统和中间件差异的绕过。Windows路径不区分大小写,而且支持正反斜杠混合;Linux下//和/等效。用这些特性可以绕开简单的字符串匹配。比如过滤了..但没过滤....//(双写编码),或者过滤了/但可以用\,反斜杠在某些解析场景下照样生效。

这些绕过手段的启示是:靠简单的黑名单修复、靠手工添加后缀限制,都不可靠。只有白名单才是真正能堵住这个洞的方案,后面我再细说。

3. 如何发现文件包含漏洞:黑盒与白盒两个方向

发现文件包含漏洞的思路分为两条线,一条是从外部测试,一条是从代码层面审计。两个方向各有优劣,也各有局限,结合起来效率最高。

3.1 黑盒测试:从URL特征找突破口

外部测试最重要的是观察URL参数的特征。只要看到参数名像page、lang、file、template、path、action、dir这类,且参数值是一个文件路径或者看起来像文件名,就得留个心眼。

具体操作方法是:先把参数值替换成一个确定的普通值,观察页面表现;再把参数值改成不存在的路径,看报错信息;再把参数值改成常见的敏感文件路径,比如/etc/passwd、../../../../etc/passwd,观察响应体内容。

我在测试时不会一上来就疯狂试路径。先分析请求和响应的对应关系,判断这个参数是不是真的被include使用了。有一个很实用的观察点:如果参数值变化时页面整体模板结构变了,说明这是页面级的include;如果只是页面内某个模块变了,可能只是数据查询逻辑。前者出错的风险更高。

还一个技巧是看响应头里的X-Powered-By或者报错页面的版本信息,确认是不是PHP环境。PHP的报错页面特征明显,如果能看到Parse error字样,基本可以确定是PHP解析了文件内容。这种情况说明include已经走到了“执行”这一步,漏洞级别直接拉满。

3.2 白盒审计:直接找危险函数

有源码的情况下,直接搜危险函数调用是最直观的方式。PHP里重点搜索include、include_once、require、require_once,Java里搜include指令和import动态拼接的场景。

找到函数调用后,重点看文件路径的构成。如果路径里出现了$_GET、$_POST、$_REQUEST、$_COOKIE等超全局变量,或者这些变量经过了模板引擎、配置文件的转发,就要确认变量的值是否可被外部控制。还有一个容易漏的点:有些参数不是直接从请求里来,而是从数据库、缓存、session里来。间接输入同样是输入,如果存储过程里可控,一样会被攻击。

白盒审计里还会遇到二次包含的写法。比如有个公共文件统一做include操作,路径通过配置文件定义,而配置文件可以被其他漏洞写入修改。这种间接路径最难发现,审计时要把数据流从头捋到底,不能只看include那一行代码。

3.3 自动化探测与手工验证的组合

自动化扫描器对文件包含的检测效果其实一般,因为文件包含的漏洞特征不像SQL注入那么明显,参数值怎么构造才有效,跟业务代码的写法强相关。扫描器只能覆盖最经典的路径穿越和常见伪协议,碰到业务层的过滤规则就容易被绕过去。

我的习惯是用自动化工具做前期的信息收集和参数识别,把带文件特征的参数全部列举出来,然后手工验证重点参数。手工验证时用Burp Suite重发请求,逐步调整payload。一个我常用的验证思路是:先发一个正常的请求,确认响应;再发一个带有php://filter/base64编码的请求,看响应里有没有base64特征字符串;如果有,解码内容是否符合目标文件的内容格式。这个验证方式成功率高,误报率也低。

另外,日志分析也是个有效的辅助手段。如果服务器日志里出现了大量路径穿越特征串(../、%00、php://等),说明已经有人在试探,同时也说明系统很可能存在这个漏洞。这类日志特征在攻防演练和入侵排查时尤其管用。

4. 文件包含漏洞的修复与防护实践

修复文件包含漏洞,很多团队第一反应是“把用户输入过滤一遍”,比如禁止../,禁止http,禁止php://。我在项目里跟开发团队反复强调过:过滤只能提高攻击门槛,做不到安全保证。最稳的方案永远是白名单。

4.1 从根上杜绝:白名单与路径规范化

白名单方案很朴素但非常有效。如果系统需要在固定的几个页面或模板之间切换,就在代码里维护一个允许的列表,用户参数只能作为查询这个列表的索引,不能直接参与路径拼接。

$allowed_pages = [ 'home' => 'pages/home.php', 'about' => 'pages/about.php', 'contact'=> 'pages/contact.php' ]; $page = $_GET['page']; if (isset($allowed_pages[$page])) { include($allowed_pages[$page]); } else { include($allowed_pages['home']); }

这样用户传入什么内容都不影响include的目标,因为include的只有代码里写死的那个值。这是根治法,无论怎么绕过,都碰不到include的参数构造。

如果业务上确实需要更灵活的文件加载路径,退而求其次的做法是路径规范化加目录约束。用realpath()函数把路径转换成绝对路径,再检查这个绝对路径是否在以站点根目录为起点的允许目录白名单内。realpath()会把../和符号链接都解析掉,能很大程度减少路径穿越的绕过空间。

$base_dir = '/var/www/html/modules/'; $user_path = realpath($base_dir . $_GET['module']); if ($user_path && strpos($user_path, $base_dir) === 0) { include($user_path); } else { // 拒绝访问 }

注意:realpath()只能处理服务器上真实存在的路径,传入一个不存在的路径会返回false,需要在业务逻辑里处理这种情况。另外,realpath()解析的是文件系统层面的路径,如果操作对象是伪协议(php://filter),realpath()根本不生效,所以这个方法只适用于固定目录下的真实文件加载。

4.2 禁用危险机制与最小化暴露面

PHP配置里有两个参数和文件包含直接相关。

第一个是allow_url_include,必须设为Off。这个参数控制include和require是否允许加载远程URL文件。PHP 5.2之后默认就是Off,但还是有不少老环境、集成环境里被改成On。把allow_url_fopen也设为Off,可以从根上堵住php://input、data://这类伪协议的利用路径。

第二个是open_basedir,它可以限制PHP能够访问的目录范围。比如设置成站点根目录,PHP脚本就只能读取这个目录下的文件,/etc/passwd、/proc这类敏感路径直接被拒。open_basedir不是万能的,它对某些伪协议有限制,但作为纵深防御的一环很值得加。

中间件层面也要有对应的限制。Nginx的fastcgi参数、Apache的配置,可以把请求URI里的异常字符直接拒绝掉。比如对包含..、%00、php://filter等特征的URL返回403,这能在请求到达业务代码之前拦掉一部分攻击尝试。

4.3 安全编码规范与漏洞修复实例

代码层面的修复要变成规范和习惯,而不是每次漏洞报了才去补。我给自己和团队定了几条规矩:

  • 任何文件加载操作都必须基于服务端认定的白名单,不直接使用客户端传入的文件名。
  • 如果必须拼接路径,先做路径规范化,再校验目录归属。
  • 不要在可写目录存放可执行的脚本文件,日志、上传文件都放在Web根目录之外,或者通过Nginx配置禁止这些目录解析PHP。
  • 定期扫描代码里的include、require调用,把动态include作为一种坏味道,评审时重点关注。

举一个修复实例。之前遇到一个后端管理系统,菜单功能是动态加载的,URL里带一个module参数:

/admin/index.php?module=system

代码是这样:

$module = $_GET['module']; include('modules/' . $module . '.php');

看得我直摇头,这几乎是最典型的文件包含写法。修复时我建议开发改成白名单映射。把系统内所有合法模块写入一个数组,module参数只用来匹配数组的键,即使传进来一个刁钻的值也只会落到默认模块。改动量不大,业务功能完全不受影响,但漏洞直接关闭。开发当时还担心影响灵活性,实际上这类模块加载场景本身模块数量就是确定的,白名单完全没有副作用。

4.4 利用防护设备做主动拦截

在没有源码、不能快速修复的场合,WAF之类的防护设备能起到临时兜底作用。文件包含的攻击特征相对明显,经典的路径穿越、伪协议、编码变体都能写成规则。

比较有效的规则思路:

  • 检查URL中是否包含../及其URL编码变体(%2e%2e、..%2f、%252e等),特别是连续出现两次以上。
  • 检查参数中是否包含php://、data://、file://、phar://等协议关键字。
  • 检查参数中是否包含系统敏感路径,如/etc/passwd、C:/Windows/win.ini、/proc/self。
  • 检测请求体中是否包含<?php等动态代码片段,防止日志注入和输入流包含。

拦截规则有个矛盾点:特征写得严格了误伤业务,写得宽了又漏绕过。比较稳妥的做法是防护设备先开告警模式,观察一段时间,把正常业务里的误报抑制掉之后,再逐步切换成阻断。

需要明确的是,WAF终究是堵漏,不是治本。攻击者只要花时间研究规则特征,总能构造出绕过payload。真正可靠的还是代码层面的白名单修复,这一点不能含糊。

5. 实战中的常见问题与排查速查

文件包含漏洞从发现到修复,路径上会遇到不少具体问题。我把这些年工作中踩过的坑整理成一份速查表,配合排查思路,希望能帮你少绕弯。

症状可能原因排查方法与处理建议
include后页面白屏目标文件不存在或权限不足检查文件路径和open_basedir配置,先确认文件是否可读
包含日志文件后报语法错误日志内容不能作为合法PHP代码解析换一种日志投毒手法,或改用session文件、临时文件
php://filter读取内容为空目标文件路径写错或文件为空先确认目标路径,用真实存在的文件测试
allow_url_include已关闭但格式仍可绕过遇到php://input或data://方式这类方式不依赖allow_url_include,拦截点应在参数白名单
WAF拦截了正常业务请求规则特征写得太宽开启告警模式,梳理误报样本,收紧规则范围
修复后发现业务页面无法正常加载白名单列表遗漏了合法模块上线前检查白名单是否覆盖全部正常业务路径

还有几个排查思路值得单独说说。

一个典型的坑是路径深度计算不准。从应用目录跳到根目录,中间隔了几层,不同框架目录结构差异很大。测试时先用一个确定存在的文件试,比如../../../../etc/passwd,不行就一层层往上加。不要一上来就蒙一个深度,很容易因为路径不对导致误判。

另一个坑是编码问题导致的漏报。URL里的参数经过浏览器和服务器双重解码,Burp Suite的重发器里要观察解码后的实际内容。有时候扫描器报的是编码后的payload,手工验证时手动解码,才发现实际读取的是另一个路径。排查时保持编码和解码的状态清晰,能少很多无谓的猜疑。

还有一个经常被忽略的点:文件包含漏洞不只是PHP的专利。Java Web(JSP)里如果用了动态include指令且路径可控,同样存在这类问题。遇到非PHP环境,别急着下结论说没有文件包含,先看代码逻辑和框架机制。中间件的SSI(Server Side Include)配置不当也是同类风险,排查时把视角放宽一些。

结尾

文件包含漏洞在我眼里是一个很好的入门级“思维训练场”。它的原理不复杂,但利用链可以拉得很长,从读文件到执行代码,中间涉及协议、系统特性、中间件配置、编码规则好多个层面。把这条链摸透了,再去看命令注入、反序列化、SSRF这类漏洞,很多思路都是相通的。

我在评估项目时有个习惯,看到include、require这类函数总会多问一句:这里的路径是开发者自己定的,还是用户扔进来的?这个简单的追问,帮团队避过不少雷。如果你在代码审查里遇到类似的动态包含写法,建议先按高危处理,把数据流完整理顺了再评估。安全这件事,从来都是宁可多查一步,也不能心存侥幸。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询