PHP文件包含漏洞实战:从原理到绕过技巧与防御策略

PHP文件包含漏洞实战:从原理到绕过技巧与防御策略 1. 项目概述一次典型的Web文件包含漏洞实战最近在复盘一些经典的CTF题目正好重新审视了BUUCTF平台上那道名为[BSidesCF 2020]Had a bad day的Web题。这道题可以说是文件包含漏洞的一个非常标准且教学意义十足的案例。它没有复杂的绕过也没有层层嵌套的过滤核心就是考察对PHP文件包含函数基本原理的理解以及如何利用这种理解去“看到”服务器上本不该被直接访问的文件。对于刚接触Web安全的新手来说这道题能帮你建立起对文件包含漏洞最直观的认知而对于有一定经验的选手它也是一个很好的提醒让你审视自己在面对一个看似简单的输入框时第一反应会是什么。简单来说题目呈现给我们的是一个带有情绪化标题“Had a bad day”的网站页面上有几个动物图片的按钮点击后会显示对应的动物照片。前端的逻辑是通过一个GET参数来动态加载图片。我们的目标就是利用这个参数突破它原本加载图片的限制去读取服务器上的其他敏感文件比如网站的源代码或者系统配置文件最终找到隐藏的flag。整个过程就像是在一栋大楼里你发现有一扇门参数输入点的锁芯结构很简单通过一些技巧你可以用这把钥匙打开这栋楼里其他的房间门甚至找到管理员藏在某个房间里的“宝藏”flag。2. 漏洞原理深度解析为什么参数可控是危险的在深入解题之前我们必须先彻底搞懂“文件包含漏洞”到底是什么以及它为什么危险。这不仅仅是知道一个名词而是要理解其背后的运行机制。2.1 动态包含的本质现代Web应用为了代码的复用性和可维护性经常会将一些公共的功能比如数据库连接、头部导航、尾部信息写在一个独立的文件里。在需要用到这些功能的地方通过特定的函数将这个文件的内容“包含”进来。在PHP中最常用的就是include()、require()、include_once()和require_once()这几个函数。设想一个场景网站有一个页面模板每个页面的头部都一样。开发者不会在每个页面都写一遍头部HTML代码而是会创建一个header.php文件然后在每个页面开头写上include(‘header.php‘);。这样当用户访问任何页面时服务器都会先读取header.php的内容将其“插入”到当前页面中再一起发送给用户。问题的核心就出在这里如果这个被包含的文件路径是由用户通过参数比如URL中的?pagexxx来控制的并且开发者没有对这个参数进行严格的过滤那么攻击者就可以通过修改这个参数让服务器去包含任意文件。2.2 本地文件包含与远程文件包含文件包含漏洞主要分为两类本地文件包含攻击者可以包含服务器本地的文件。就像这道题里我们最终是通过包含/etc/passwd或PHP文件来获取信息。这是最常见的一种。远程文件包含如果服务器的PHP配置中allow_url_include选项为On攻击者甚至可以包含一个远程服务器上的文件例如http://evil.com/shell.txt让目标服务器执行这个远程文件中的PHP代码。这种危害性极大但现代PHP版本默认是关闭的因此LFI更为常见。2.3 漏洞的深远影响文件包含漏洞的危害远不止读取几个文件那么简单它往往是通往更大漏洞的跳板敏感信息泄露直接读取/etc/passwd查看系统用户读取/proc/self/environ获取环境变量可能包含密钥读取Web日志文件、配置文件如config.php里面常有数据库密码。源码审计通过包含Web应用的源码文件.php如果服务器配置不当有时会直接以源代码形式显示从而进行白盒审计发现更多逻辑漏洞。获取WebShell这是最危险的一步。通过结合文件上传漏洞或者利用PHP的某些特性如php://input、php://filter、日志注入等攻击者有可能在服务器上执行任意代码完全控制网站。拒绝服务包含一个特别大的文件或者一个会陷入循环的设备文件如/dev/zero可能导致服务器资源耗尽。理解了这些我们再回头看Had a bad day这道题就能明白它实际上是把一个复杂的漏洞模型简化成了一个最纯净的“输入参数可控”的实验环境让我们可以心无旁骛地练习最核心的利用技巧。3. 靶场环境侦察与初步测试面对任何Web题目第一步永远是信息收集和黑盒测试盲目猜测是效率最低的做法。3.1 界面交互与功能分析打开题目链接我们看到一个简单的页面上面写着“Had a bad day? Me too. Feel free to look at these animal photos to lighten up your day!”。下方有三个按钮分别对应猫、狗和刺猬的图片。点击任何一个按钮比如“dog”页面上会显示一张狗的图片同时浏览器的URL会发生变化。原始URL可能是http://target.com/index.php点击后变成了http://target.com/index.php?categorydog。图片的显示地址可能是类似src“images/dog.jpg”这样的形式。这里第一个关键点出现了URL中出现了category这个GET参数。它的值dog直接对应了显示的图片内容。一个合理的推测是后端代码逻辑是这样的$category $_GET[‘category‘]; include(‘./images/‘ . $category . ‘.jpg‘); // 或者更可能是include(‘./images/‘ . $category . ‘.php‘); 但用.jpg伪装无论具体实现如何category参数的值被拼接进了文件路径并最终被include或类似函数处理。这就是典型的文件包含漏洞触发点。3.2 基础Payload测试与错误回显我们的测试就从修改这个category参数开始。直接访问?categorydog是正常功能。我们尝试一些基础的测试Payload目录遍历测试?category../../../../etc/passwd意图尝试跳出当前images目录去访问系统根目录下的/etc/passwd文件。可能结果如果存在漏洞且无过滤可能会直接显示出passwd文件的内容。如果被过滤可能会看到报错、空白页或原功能图片。PHP伪协议测试?categoryphp://filter/readconvert.base64-encode/resourceindex.php意图使用PHP的php://filter伪协议以Base64编码的方式读取index.php的源代码。这样可以避免代码被直接执行而是以文本形式显示。为什么用Base64编码因为如果直接包含index.php其中的PHP代码会被服务器执行我们看不到源代码。通过过滤器先进行Base64编码PHP引擎会将其当作一个Base64编码的文本流处理从而输出编码后的字符串我们解码后就能得到源码。绝对路径测试?category/etc/passwd意图尝试直接使用绝对路径。在Had a bad day这道题中我们很快会发现直接使用../../或php://可能会遇到阻碍。题目通常会设置一些简单的过滤。这时观察错误回显就至关重要。如果页面返回了一个Warning或Error其中包含了被拼接后的完整文件路径那将是极有价值的信息。例如可能会提示include(./images/../../etc/passwd.jpg): failed to open stream。这个错误信息明确告诉我们两件事第一我们的输入被拼接了.jpg后缀第二服务器确实尝试去包含我们构造的路径。4. 核心绕过技巧后缀截断与路径拼接承接上面的错误信息我们发现了本题的第一个关键过滤后端会自动为category参数的值添加.jpg后缀。也就是说无论我们输入什么最终服务器尝试打开的文件都是./images/【我们的输入】.jpg。这直接阻止了我们直接包含/etc/passwd因为会变成/etc/passwd.jpg文件不存在或index.php变成index.php.jpg。4.1 利用字符串截断漏洞在早期PHP版本5.3.4中存在一个经典的漏洞当include/require的文件路径字符串中包含空字符%00即URL编码后的NULL字节时PHP会在空字符处截断字符串。这意味着我们可以这样构造Payload?category../../../../etc/passwd%00服务器拼接后得到./images/../../../../etc/passwd%00.jpg。在PHP旧版本处理时遇到%00就认为字符串结束因此实际尝试包含的文件是./images/../../../../etc/passwd成功绕过了.jpg后缀。然而在现代PHP环境本题环境通常较新中这个漏洞已被修复%00截断不再有效。4.2 利用路径长度截断这是另一个历史技巧依赖于操作系统文件系统对路径最大长度的限制。原理是构造一个超长的路径使得系统自动截断后面的部分包括我们讨厌的.jpg。但由于可靠性差且依赖特定环境在现代CTF和实战中已很少见。4.3 本题的正解利用目录遍历“吃掉”后缀既然后缀是硬性添加的我们能不能让这个后缀变得无关紧要仔细观察错误信息或正常请求?categorydog最终访问的是./images/dog.jpg。如果我们输入?categorydog/../cat呢 服务器会拼接成./images/dog/../cat.jpg。在文件系统路径解析中dog/../意味着先进入dog子目录然后返回上级目录等价于当前目录。所以最终路径被解析为./images/cat.jpg。这看起来没什么用。关键的思路转换如果我们让遍历的层级更深让..跳出images目录呢例如?category../../../etc/passwd拼接后./images/../../../etc/passwd.jpg路径解析./images/../../../等于../../从images目录向上跳三级回到了Web根目录的上级。所以最终解析为../../etc/passwd.jpg。我们依然需要面对.jpg后缀。但是如果目标文件本身不存在.jpg扩展名这个请求就会失败。那么有没有一个文件它的名字里恰好包含.jpg并且其内容是我们感兴趣的呢或者有没有办法让.jpg成为目标文件路径的一部分而不是一个多余的后缀这就是本题的巧妙之处。我们尝试包含一个目录。例如?category../../../etc/passwd/拼接后./images/../../../etc/passwd/.jpg路径解析后服务器会尝试访问/etc/passwd/.jpg。在Linux系统中/etc/passwd是一个文件/etc/passwd/试图将其当作目录访问这通常会失败返回is not a directory错误。此路不通。真正的突破口在于PHP伪协议。我们尝试?categoryphp://filter/readconvert.base64-encode/resourceindex.php拼接后./images/php://filter/readconvert.base64-encode/resourceindex.php.jpg这时PHP引擎会首先识别php://这个流协议。对于PHP伪协议来说其后缀是什么并不重要因为PHP在处理php://filter时关注的是协议本身和其参数read...,resource...。.jpg后缀被当作伪协议路径的一部分但php://filter处理器会正确地解析resourceindex.php这个参数并返回index.php文件经过Base64编码后的内容。.jpg后缀被默默地忽略在了协议路径的末尾没有造成实质影响。实操心得当遇到自动添加后缀的过滤时第一时间应该想到PHP伪协议。因为伪协议的处理优先级和方式与普通文件系统路径不同后缀名往往不构成障碍。这是非常高频且有效的绕过手段。5. 利用PHP Filter协议读取源码通过上面的分析我们确定了使用php://filter伪协议来读取源码是可行的。现在我们来实施。5.1 构造Payload读取入口文件我们访问http://靶机地址/index.php?categoryphp://filter/readconvert.base64-encode/resourceindex.php如果漏洞存在且过滤可绕过服务器将返回一串Base64编码的字符串。我们可能会看到类似这样的输出混杂在HTML中...一些HTML... PD9waHAg...很长一串Base64... ...更多HTML...或者页面可能直接显示一大段纯粹的Base64字符串。我们将这段Base64字符串复制下来使用任何解码工具进行解码。在线解码网站、Linux系统的base64 -d命令、或者编程语言中的解码函数都可以。解码后我们得到了index.php的源代码。这是至关重要的一步因为源码会告诉我们后端的完整逻辑、可能的过滤规则、以及下一步的方向。5.2 源码审计与逻辑分析假设我们解码后得到的index.php源码核心部分如下此为模拟实际题目可能略有不同?php $file $_GET[‘category‘]; if(isset($file)) { if(strpos($file, “woofers“) ! false || strpos($file, “meowers“) ! false || strpos($file, “index“)) { include($file . ‘.php‘); } else { echo “Sorry, we only have woofers and meowers and index.phps here.“; } } else { header(‘Location: ?categorywoofers‘); exit; } ?代码逻辑解读获取category参数存入$file变量。检查$file中是否包含子字符串”woofers“、”meowers“或”index“。如果包含则执行include($file . ‘.php‘);。注意这里添加的后缀是.php不是.jpg这解释了为什么之前测试dog和cat能显示图片因为前端按钮可能链接到?categorywoofers/dog和?categorymeowers/cat这样的形式满足了检查条件。如果不包含则输出错误信息。这个逻辑存在一个明显的缺陷检查是宽松的“包含”检查而非严格的“等于”检查。只要我们的输入里含有woofers、meowers或index这三个词中的任意一个就能通过检查。5.3 构造绕过检查的Payload我们的目标是读取index.php的源码我们已经有了php://filter/readconvert.base64-encode/resourceindex.php这个Payload。但它不包含woofers等关键词会被拦截。绕过方法我们可以利用字符串拼接让Payload中既包含关键词又能让PHP正确解析伪协议。例如?categorywoofers/../../../php://filter/readconvert.base64-encode/resourceindex.php让我们拆解一下$file的值是woofers/../../../php://filter/readconvert.base64-encode/resourceindex.phpstrpos($file, “woofers“)返回0存在通过检查。后端执行include($file . ‘.php‘);即include(‘woofers/../../../php://filter/readconvert.base64-encode/resourceindex.php.php‘);路径解析woofers/../../../等价于../../假设woofers是当前目录下的一个子目录。PHP引擎识别到php://filter协议并处理resourceindex.php参数。末尾的.php后缀同样被附加到了协议路径后但如前所述php://filter处理器会忽略它正确读取index.php文件并返回其Base64编码内容。通过这种方式我们成功绕过了关键词检查并利用文件包含漏洞读取到了index.php的源码。6. 漏洞利用的深入获取Flag在CTF题目中读取源码往往不是终点flag通常藏在另一个文件里。我们需要从源码中寻找线索。6.1 分析源码寻找提示仔细审计我们获取到的index.php源码。除了核心逻辑我们可能还会发现一些注释、被引用的其他文件、或者硬编码的路径。例如源码中可能有一行// The flag is in /flag.txt或者在包含文件的部分可能引用了其他PHP文件include(‘./includes/header.php‘); include(‘./includes/config.php‘);config.php这类文件常常是敏感信息所在。我们需要尝试读取这些文件。6.2 利用相同手法读取其他文件假设我们猜测flag在/flag.txt。我们构造Payload?categorywoofers/../../../php://filter/readconvert.base64-encode/resource/flag.txt同样它满足包含woofers的条件并且利用伪协议读取根目录下的flag.txt文件。将返回的Base64字符串解码就能得到明文flag。如果flag在config.php中则Payload为?categorywoofers/../../../php://filter/readconvert.base64-encode/resourceconfig.php或者使用相对路径?categorywoofers/../../../php://filter/readconvert.base64-encode/resource./includes/config.php6.3 尝试直接包含执行PHP代码在某些变种或更简单的题目设置中如果过滤不严我们甚至可以尝试直接包含一个可写的日志文件并在User-Agent等HTTP头中注入PHP代码从而实现远程代码执行。但在Had a bad day这道题中利用伪协议读取文件已经足够获取flag通常不需要走到RCE那一步。7. 防御策略与安全编程思考通过这道题我们不仅学会了攻击更应该学会如何防御。作为开发者应该如何避免此类漏洞避免动态包含这是最根本的。如果可能使用静态包含或模板引擎。如果必须动态则使用白名单机制。实施严格的白名单不要使用黑名单禁止../等而应该使用白名单。只允许包含预设的几个文件。$allowed_files [‘home.php‘, ‘about.php‘, ‘contact.php‘]; $file $_GET[‘page‘]; if (in_array($file, $allowed_files)) { include(‘./pages/‘ . $file); } else { include(‘./pages/404.php‘); }路径规范化与过滤如果白名单不可行必须对用户输入进行严格处理。使用basename()函数获取路径中的文件名部分去除目录遍历字符。使用realpath()函数解析绝对路径并检查该路径是否在允许的目录范围内。$base_dir ‘/var/www/html/pages/‘; $user_file $_GET[‘page‘]; $real_path realpath($base_dir . $user_file); // 检查$real_path是否以$base_dir开头防止目录穿越 if ($real_path strpos($real_path, $base_dir) 0) { include($real_path); } else { die(‘Invalid file.‘); }关闭危险配置确保php.ini中allow_url_include和allow_url_fopen设置为Off防止远程文件包含。设置open_basedir在PHP配置中限制文件操作只能发生在指定目录树内为漏洞利用增加一层障碍。8. 常见问题与排查技巧实录在实际操作和教学过程中我遇到过很多同学在解这类题目时遇到的坑。这里记录一下问题1使用Payload后页面空白或报错“No such file or directory”。排查首先检查Payload的语法是否正确特别是php://filter的格式。确保read和resource参数正确。其次检查路径。使用绝对路径/var/www/html/index.php有时比相对路径index.php更可靠。最后确认是否满足了题目设定的过滤条件如本题的woofers关键词检查。问题2返回了Base64字符串但解码后是乱码或不是PHP源码。排查这可能是因为包含的文件不是文本文件如图片或者包含过程中发生了错误输出的Base64其实是错误信息本身。确认resource参数指向的文件确实存在且是文本文件。另外有些题目可能会对输出进行额外的编码或混淆需要仔细观察Base64解码后的内容看是否有HTML注释、JS代码等包裹着真正的flag。问题3%00截断无效。确认这几乎可以确定是PHP版本5.3.4。不要再依赖%00截断转而使用伪协议或路径长度截断较少用等其他方法。问题4伪协议被禁用。排查如果php://协议被服务器禁用页面可能会报错或直接忽略。此时需要尝试其他方法比如利用文件上传、日志包含等。但在标准CTF题中伪协议通常可用。问题5如何确定自动添加的后缀是什么技巧通过观察正常请求和错误信息。例如访问?categorytest如果报错提示test.jpg未找到则后缀是.jpg如果提示test.php未找到则是.php。也可以尝试访问一个肯定不存在的值如?category“从错误信息中分析拼接后的完整字符串。这道[BSidesCF 2020]Had a bad day题目就像它的名字一样如果你不理解文件包含的基本原理和绕过技巧确实会让人“have a bad day”。但一旦你掌握了“参数可控”这个核心以及“伪协议绕过后缀”这个关键技巧问题就迎刃而解了。它完美地展示了安全漏洞往往源于最微小的逻辑疏忽——一个未经验证的用户输入直接拼接进了敏感函数。在平时开发中务必对任何来自用户的数据保持警惕实施最小权限和严格验证的原则。而在渗透测试或CTF中遇到文件包含点伪协议永远是你的第一把利器。