PHP文件包含漏洞实战:绕过白名单校验与路径解析差异利用

发布时间:2026/7/27 6:10:28
PHP文件包含漏洞实战:绕过白名单校验与路径解析差异利用 1. 项目概述一次关于PHP文件包含漏洞的深度实战复盘最近在复盘一些经典的Web安全挑战题特别是攻防世界CTF平台上的“warmup”这道题它可以说是PHP文件包含漏洞File Inclusion Vulnerability的一个绝佳教学案例。这道题的精妙之处在于它没有使用常规的本地文件包含LFI或远程文件包含RFI那种“直给”的方式而是设置了一个看似安全的“白名单”检查机制。很多新手甚至一些有一定经验的朋友初次接触时都可能卡住因为它要求我们不仅要理解文件包含的原理更要深入理解服务器在处理请求时代码逻辑、文件系统与Web服务器配置三者之间微妙的交互关系。这不仅仅是解一道题更是理解现代Web应用中一种常见安全设计缺陷及其绕过思路的实战演练。简单来说这个项目就是深入剖析“warmup”题目中如何利用PHP文件包含漏洞并成功绕过其白名单限制最终读取到目标文件比如flag的过程。我们将从漏洞原理、代码审计、环境交互、路径构造等多个维度一步步拆解攻击链。无论你是正在学习Web安全的新手希望巩固文件包含漏洞知识的中级选手还是想了解一些不常见的绕过技巧的安全爱好者这篇复盘都能给你带来清晰的思路和可直接复现的实操经验。接下来我们就抛开那些抽象的理论直接进入“案发现场”看看这道题到底是怎么被“玩转”的。2. 漏洞核心原理与“warmup”场景还原在开始拆解具体技巧前我们必须把地基打牢。PHP文件包含漏洞核心源于include、require、include_once、require_once这四个函数。当这些函数包含的文件路径来自于用户可控的输入如$_GET、$_POST、$_COOKIE等且未经过严格过滤时攻击者就有可能操纵这个路径让服务器去执行或读取非预期的文件。2.1 文件包含漏洞的两种基本形态本地文件包含LFI包含服务器本地的文件。例如include($_GET[‘file’]);如果传入?file../../etc/passwd就可能读取到系统敏感文件。远程文件包含RFI包含远程服务器上的文件。这需要php.ini中allow_url_include设置为On默认是Off风险极高可以导致直接执行远程恶意代码。“warmup”这道题通常属于LFI的范畴但它的难点不在于简单的目录遍历../而在于它有一个白名单校验。题目源码模拟的关键部分往往如下所示?php highlight_file(__FILE__); class emmm { public static function checkFile($page) { // 白名单列表 $whitelist [source.php, hint.php]; // 检查$page是否在白名单内 if (! isset($page) || !is_string($page)) { echo you cant see it; return false; } // 关键检查$page 必须是以白名单内某个文件开头的字符串 if (in_array($page, $whitelist)) { return true; } $_page mb_substr($page, 0, mb_strpos($page . ?, ?)); if (in_array($_page, $whitelist)) { return true; } $_page urldecode($page); $_page mb_substr($_page, 0, mb_strpos($_page . ?, ?)); if (in_array($_page, $whitelist)) { return true; } echo you cant see it; return false; } } if (! empty($_REQUEST[file]) // 检查file参数是否存在且非空 is_string($_REQUEST[file]) // 检查是否为字符串 emmm::checkFile($_REQUEST[file]) // 调用白名单检查函数 ) { include $_REQUEST[file]; // 包含文件 } else { echo brimg src\https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\ /; } ?2.2 “warmup”场景的独特设定一眼看去这个检查似乎很严格$page参数必须直接是source.php或hint.php或者是一个以它们开头、后面跟了?的字符串。这看起来万无一失因为即使你传入source.php/../../../../etc/passwd经过mb_substr($page, 0, mb_strpos($page . ?, ?))处理截取第一个?之前的部分得到的是source.php依然在白名单内检查通过。但是真正的漏洞就隐藏在“检查通过”之后的那行代码include $_REQUEST[‘file’];。这里存在一个检查点与执行点分离的经典问题。checkFile函数检查的是处理后的$_page而最终执行include使用的是原始的$_REQUEST[‘file’]。只要我们能构造一个payload让它通过前者的校验同时又能让后者解析成我们想要的路径漏洞就产生了。关键理解include函数在解析文件路径时其行为会受到服务器环境如PHP版本、操作系统和Web服务器如Apache、Nginx配置的深刻影响。尤其是在处理包含路径中的特殊字符如/、?、#时不同组件的解析差异就是我们绕过的突破口。3. 白名单绕过的核心技巧路径解析差异的利用要让原始的$_REQUEST[‘file’]被include函数解析成其他路径我们需要利用Web服务器和PHP在解析URL和文件路径时的特性。主要有以下几种思路而“warmup”通常考察的是第一种。3.1 利用?进行路径截断经典解法这是本题最直接的解法。观察checkFile函数它用mb_strpos($page . ‘?’, ‘?’)来寻找第一个问号的位置。如果我们传入source.php?../../../../../etc/passwd函数逻辑如下$page值为source.php?../../../../../etc/passwd。$page . ‘?’变成source.php?../../../../../etc/passwd?。mb_strpos(…, ‘?’)找到第一个问号的位置在source.php后面。mb_substr($page, 0, 上述位置)截取出source.php。source.php在白名单中checkFile返回true。此时原始的参数$_REQUEST[‘file’]仍然是source.php?../../../../../etc/passwd。当执行include ‘source.php?../../../../../etc/passwd’;时PHP的include函数会如何理解这个字符串PHP的include机制include会把传入的字符串当作一个文件路径。当路径中包含?时PHP会默认将?及其之后的部分视为查询字符串query string而不是路径的一部分。在尝试打开文件时PHP会忽略?后面的所有内容。也就是说对于include ‘source.php?../../../../../etc/passwd’;PHP实际尝试打开的文件就是source.php。那岂不是没用别急这里需要引入第二个角色Web服务器以Apache为例。当浏览器发起一个请求http://target.com/index.php?filesource.php?../../../../../etc/passwd时这个URL会被Web服务器如Apache首先接收并解析。Apache的解析规则是最后一个?之后的内容是HTTP请求的查询字符串QUERY_STRING而?之前的部分是请求的路径PATH_INFO或实际文件路径。但是在这个URL中出现了两个?这通常是不规范的。Apache的默认模块mod_cgi或mod_php在处理时可能会将第一个?之后的所有内容整体作为查询字符串传递给PHP。然而这里存在一个关键点在将路径映射到实际文件系统时Apache可能会对URL中的..进行目录回溯解析。更常见的利用方式是结合路径..和伪协议php://filter。但“warmup”题目通常设计为直接包含flag文件。假设通过信息收集比如包含hint.php我们得知flag文件在/ffffllllaaaagggg一个虚构的根目录文件。我们的Payload可以构造为?filesource.php?/../../../../ffffllllaaaagggg对checkFile函数它截取第一个?之前的部分得到source.php通过检查。对PHP的include它接收到的是source.php?/../../../../ffffllllaaaagggg。PHP会尝试打开名为source.php的文件并将?/../../../../ffffllllaaaagggg视为一个无效的查询字符串而忽略吗不一定。在某些环境下特别是当路径中包含多个/时PHP的文件系统函数在解析时可能会因为?的存在而产生非预期的行为。但更通用和可靠的理解是我们需要让?后面的路径在某个环节被解析。实际上更精确的利用依赖于PHP在特定配置下对include路径中?的模糊处理以及路径中/的数量。一个经过验证的有效Payload是?filesource.php?../../../../../ffffllllaaaagggg。在某些PHP版本和服务器配置下include会错误地将整个字符串作为路径而?被当作合法文件名的一部分虽然不存在但利用../成功跳出了当前目录。另一种可能是Web服务器在将请求传递给PHP前对URL进行了一些重写或解码导致了路径解析的歧义。3.2 利用#锚点进行截断#在URL中表示网页锚点其后的内容不会发送到服务器。但在PHP代码中如果直接将#写入include的字符串例如include ‘source.php#../../../../etc/passwd’;PHP的include函数会怎么处理在Linux系统下#是合法的文件名字符。PHP会尝试寻找一个名为source.php#../../../../etc/passwd的文件这显然不存在。因此纯#截断在文件包含中通常无效除非结合其他技巧如NULL字节截断但已在PHP高版本修复。3.3 利用编码与双重编码这是更高级的绕过方式常用于应对更复杂的过滤。checkFile函数中有一行$_page urldecode($page);说明它对输入进行了一次URL解码。这本身是一个安全措施旨在防止编码绕过。但如果我们进行双重编码呢我们想传入source.php?../../../../etc/passwd。服务器收到后会默认进行一次URL解码。如果我们直接传?会被解码为?。但是如果我们把?编码两次%253f%25是%的编码3f是?的编码。服务器第一次解码%253f-%3f因为%25被解码成了%。checkFile函数执行urldecode对%3f进行解码得到?。此时它截取判断可能因为?已经出现而通过检查取决于代码逻辑在warmup原题中它是在urldecode后再次截取判断所以依然可能通过。而最终include使用的$_REQUEST[‘file’]在PHP获取时已经经过服务器第一次解码即source.php%3f../../../../etc/passwd。在某些特定环境或配置下PHP的include可能将%3f仍当作字面字符%3f处理从而无法正确识别?的截断作用导致整个字符串被当作路径使得../生效。或者在另一些场景下文件系统调用层面对编码的解析差异可能导致漏洞。在“warmup”原题中通常不需要用到双重编码这么复杂第一种?截断就足够了。但了解这种思路对于应对更严格的过滤非常重要。实操心得在实战测试中?截断的成功率高度依赖于目标服务器的具体环境PHP版本、Web服务器类型及配置、操作系统。因此在CTF中这通常是一个“已知可用”的考点。在真实渗透测试中则需要多种Payload进行尝试并仔细观察错误信息。一个常见的测试序列是先尝试简单的../遍历再尝试?截断接着尝试....//或..;/等变形最后尝试编码绕过。4. 完整攻击链实操与步骤拆解现在我们假设在一个模拟的“warmup”靶场环境中进行完整的攻击演示。目标读取服务器上的/flag文件。4.1 信息收集与初步探测访问目标打开题目提供的URL例如http://xxx.xxx.xxx.xxx:port/。查看页面源码通常首页会给出提示或者直接展示部分源码因为题目有highlight_file(__FILE__);。我们能看到类似前面给出的PHP代码。尝试白名单文件根据代码中的白名单[“source.php”, “hint.php”]我们首先尝试访问http://xxx.xxx.xxx.xxx:port/?filesource.php- 可能会显示当前页面的源码即index.php的源码因为include了自身。http://xxx.xxx.xxx.xxx:port/?filehint.php-这是关键一步很多CTF题目会在hint.php中给出flag的路径提示。访问后页面可能会显示一行字比如flag not here, but maybe in /flag或者look at the source code of /ffffllllaaaagggg。我们假设提示是flag is in /ffffllllaaaagggg。4.2 构造绕过Payload我们已经知道检查逻辑截取第一个?之前的内容检查是否在白名单内。目标文件/ffffllllaaaagggg位于网站根目录。当前文件我们位于index.php。我们需要构造一个file参数使得截取第一个?之前的部分是source.php或hint.php。最终include整个参数时能跳转到/ffffllllaaaagggg。根据第3节的分析我们使用?截断技术。Payload构造如下?filesource.php?/../../../../../ffffllllaaaagggg路径回溯解释假设index.php位于/var/www/html/。source.php?这部分让检查通过。/../../../../../这部分是目录回溯。从/var/www/html/开始../-/var/www/../../-/var/../../../-/../../../../- 已经到根目录/继续../仍然停留在/。最终路径根目录/ffffllllaaaagggg/ffffllllaaaagggg。为什么是5个../这是一个经验值。在不确定具体目录深度时通常会使用多个../以确保能回溯到根目录。使用4个、5个、6个都是常见的做法。4.3 发起攻击并获取Flag将构造好的Payload拼接到URL后http://xxx.xxx.xxx.xxx:port/?filesource.php?/../../../../../ffffllllaaaagggg访问这个URL。如果漏洞存在且环境配置允许这种绕过服务器将执行include ‘source.php?/../../../../../ffffllllaaaagggg’;。根据我们之前的分析在特定环境下这个操作会成功包含并输出/ffffllllaaaagggg文件的内容也就是我们想要的flag。4.4 其他Payload变种尝试如果上述Payload不成功可以尝试以下变种这体现了实战中的试探过程减少../数量?filesource.php?/../../ffffllllaaaagggg假设目录较浅。使用hint.php作为前缀?filehint.php?/../../../../../ffffllllaaaagggg。去掉?后的/?filesource.php?../../../../../ffffllllaaaagggg在某些解析场景下?后的../可能被直接当作路径的一部分。尝试包含php://filter读取源码如果flag不在文件而在PHP变量中?filesource.php?/../../../../../var/www/html/index.php但这样包含的是PHP代码会被执行。可以结合php://filter/convert.base64-encode/resource来编码读取?filephp://filter/convert.base64-encode/resourcesource.php?/../../../../../var/www/html/index.php。注意此Payload需要checkFile函数能通过php://filter/...的检查在原题warmup中因为检查in_array($page, $whitelist)php://filter显然不在白名单所以此Payload无效。这里只是展示一种思路。注意事项在真实CTF或授权测试中目录深度是未知的。通常的做法是使用足够多的../比如8个或10个来确保能回溯到根目录。Linux系统的根目录/在其之上使用../仍然会停留在/所以多写几个是安全的。但在某些配置了open_basedir限制的PHP环境中过多的../可能会导致路径被拒绝。5. 漏洞挖掘与代码审计的深层思考“warmup”题目虽然简单但它揭示了文件包含漏洞尤其是带校验的包含漏洞的审计关键点。当我们审计一个PHP应用时如何系统地发现此类问题5.1 审计入口点全局搜索包含函数使用IDE或代码搜索工具查找项目中所有的include,require,include_once,require_once。追踪参数来源对于每一个包含函数向上追踪其参数即文件路径字符串的来源。重点关注来自超全局变量的数据$_GET,$_POST,$_REQUEST,$_COOKIE,$_SERVER中的某些字段如$_SERVER[‘PHP_SELF’],$_SERVER[‘REQUEST_URI’]有时也会被误用。分析过滤逻辑检查对来源数据是否有过滤。过滤方式包括白名单如本题只允许包含特定文件。黑名单过滤../,..\,php://,http://等危险字符串。前缀/后缀拼接例如include ./uploads/’ . $_GET[‘file’] . ‘.jpg’;将用户输入拼接在固定目录和扩展名中间。5.2 分析校验与执行的分离“warmup”的核心漏洞模式是校验与执行分离。审计时要特别注意是否存在一个“检查函数”如checkFile它对输入进行了处理如截取、解码、替换并返回布尔值调用该检查函数后include使用的是原始输入还是检查函数处理后的结果检查函数对输入的处理逻辑与include函数以及底层操作系统对路径的解析逻辑是否存在差异这种差异就是绕过点。字符串处理 vs 路径解析代码可能用str_replace(‘..’, ”, $input)过滤..但….//经过替换后可能又变回../。解码时机差异如urldecode在检查时用了一次但数据在进入include前是否又被解码了一次截断符号理解差异代码用?、#、\0NULL字节作为截断标记进行检查但include和文件系统是否以同样方式理解这些字符5.3 利用环境差异构造Payload即使代码逻辑看起来严密也要考虑运行环境操作系统差异Windows路径分隔符是\且不区分大小写Linux是/区分大小写。过滤了../但没过滤..\可能在Windows上生效。Web服务器重写规则Apache的mod_rewrite、Nginx的try_files或rewrite规则可能会改变请求的最终路径导致代码中的检查与实际包含的路径不一致。PHP配置magic_quotes_gpc已废弃、open_basedir、allow_url_include等设置会影响漏洞利用方式。5.4 针对“warmup”类白名单的通用测试Payload库在审计或测试时可以准备一个Payload列表进行Fuzz测试filesource.php filesource.php? # 尝试问号 filesource.php?/ filesource.php?../ filesource.php?../../../../etc/passwd filesource.php?/../../../../etc/passwd filesource.php%3f../../../../etc/passwd # 问号URL编码 filesource.php%253f../../../../etc/passwd # 双重编码 filesource.php# # 尝试井号通常无效 filesource.php/./././?/../../../../etc/passwd # 增加冗余路径 filesource.php/../../../etc/passwd?.jpg # 后缀截断尝试需特定环境 file....//....//....//....//etc/passwd # 点号变形绕过简单替换 filesource.php/../../../etc/passwd\0 # NULL字节截断PHP5.3.4将这些Payload通过Burp Suite的Intruder或自定义脚本进行批量测试观察响应差异。6. 防御策略与安全开发建议理解了攻击才能更好地防御。针对“warmup”所代表的这类文件包含漏洞我们可以从多个层面进行加固。6.1 代码层防御治本之策绝对禁止用户输入直接控制包含路径这是最根本的原则。如果业务必须动态包含则应采用以下方式使用强白名单机制白名单不应只做“开头匹配”而应做全路径映射。错误示例易绕过if (strpos($input, ‘safe/’) 0) { include $input; }正确示例$whitelist [ ‘page1’ ‘./templates/page1.php’, ‘page2’ ‘./templates/page2.php’, ]; $key $_GET[‘page’]; if (array_key_exists($key, $whitelist)) { include $whitelist[$key]; // 包含的是预定义好的固定路径 } else { include ‘./templates/default.php’; }这样用户只能通过key来选择而无法控制任何路径字符串。路径规范化与绝对路径在包含前对路径进行规范化处理并转换为绝对路径然后检查其是否在允许的目录内。$baseDir realpath(‘./allowed_directory/’); $userPath realpath($baseDir . ‘/’ . $_GET[‘file’]); if ($userPath strpos($userPath, $baseDir) 0) { // 确保$userPath在$baseDir目录或其子目录下 include $userPath; } else { die(‘Invalid file path.’); }realpath()函数会解析..和符号链接并返回绝对路径。再通过strpos检查是否在允许的基目录下。避免使用$_REQUEST$_REQUEST同时包含了$_GET,$_POST,$_COOKIE来源不可控且易混淆。明确使用$_GET或$_POST。6.2 配置层防御减少攻击面PHP配置open_basedir将PHP可访问的文件限制在指定的目录树中。即使存在包含漏洞攻击者也无法跳出这个“牢笼”。allow_url_include务必设置为Off默认值彻底关闭远程文件包含功能。disable_functions可以考虑禁用一些危险函数如pcntl_exec,proc_open,system等但这对文件包含漏洞本身防御有限主要防止包含后的代码执行。Web服务器配置为每个应用设置独立的运行用户和目录权限遵循最小权限原则。使用安全的文件上传目录将其与可执行脚本目录分离并配置为不可执行脚本。系统层防御确保Web服务器进程对敏感系统文件如/etc/passwd,/etc/shadow, 应用配置文件等只有最小读取权限最好是无权限。6.3 安全开发生命周期代码审计将安全代码审计作为开发流程的必要环节重点关注用户输入点、文件操作、数据库查询、命令执行等高风险函数。使用安全框架现代PHP框架如Laravel, Symfony在路由、视图加载等方面有更安全的机制能很大程度上避免手写代码导致的文件包含漏洞。依赖库安全定期更新框架和第三方库修复已知安全漏洞。渗透测试与漏洞扫描在应用上线前和定期进行安全测试主动发现潜在漏洞。7. 从CTF到实战的思维延伸“warmup”是一个高度简化的模型。真实世界的应用要复杂得多但漏洞原理相通。在实战中文件包含漏洞往往不会这么“赤裸裸”地出现它可能隐藏在模板引擎的加载机制中某些自定义或老旧模板引擎如果允许用户控制模板文件名可能造成文件包含。插件/模块加载功能通过参数动态加载插件文件。本地文件缓存或代理功能某些功能会根据URL参数去读取或缓存本地文件。日志文件包含如果包含漏洞存在且攻击者能控制部分内容写入日志如User-Agent可以先将PHP代码写入日志再包含该日志文件执行代码需要日志文件可读且位于Web目录下。结合文件上传这是非常经典的组合拳。先上传一个包含恶意代码的图片文件利用文件上传漏洞然后通过文件包含漏洞去包含这个图片文件从而执行代码。即使图片后缀是.jpg只要文件内容以?php ... ?开头PHP的include函数依然会尝试解析其中的PHP代码需要服务器未配置过滤或解析错误。因此在实战渗透测试中发现文件包含漏洞的步骤通常是信息收集 - 发现疑似包含点如?pageabout - 测试路径遍历../ - 测试协议封装php://filter,phar://,zip://等 - 结合其他漏洞如上传、SSRF扩大战果 - 获取Webshell或敏感信息。“warmup”这道题就像一把钥匙它打开的不是一道简单的门而是通往Web安全中一个庞大而重要的知识领域。理解它不仅是为了解一道题更是为了培养那种在复杂代码和交互中寻找“差异”和“不一致”的安全嗅觉。每次遇到校验逻辑都不妨多问一句“这里检查的数据和最终使用的数据是完全一样的吗在不同的上下文代码层、Web服务器层、操作系统层中对同一个字符串的理解会一致吗” 多问几个为什么很多漏洞的绕过思路就藏在答案里。