CTFshow文件包含漏洞实战:五种绕过姿势与防御策略详解

1. 项目概述:一次对文件包含漏洞的深度狩猎

文件包含漏洞,在Web安全领域里算是个“老朋友”了,但它的“生命力”却异常顽强。尤其是在CTF竞赛和渗透测试的实战场景中,它往往是突破边界、获取权限的关键跳板。这次我们聚焦于CTFshow平台上的文件包含挑战,目标不仅仅是“做出来”,而是要彻底搞懂从基础的PHP伪协议利用,到进阶的日志文件包含,再到各种过滤规则下的花式绕过,这背后完整的攻击链和防御逻辑。如果你对PHP的includerequire等函数如何被“玩坏”感到好奇,或者在实际测试中遇到过滤了../php://就束手无策,那么这篇从实战中总结的笔记,或许能给你带来一些新的思路。我们将拆解五种核心的绕过姿势,每一种都配有详细的原理分析、实操步骤和踩坑记录,力求让你不仅能复现,更能理解为什么这么做能行。

2. 漏洞原理与核心思路拆解

2.1 文件包含漏洞的本质是什么?

简单来说,文件包含漏洞源于应用程序在动态包含文件时,未对用户控制的输入进行严格校验。在PHP中,includerequireinclude_oncerequire_once这些函数,如果其参数(通常是文件路径)可以被攻击者操控,就可能导致非预期的文件被读取或执行。这不仅仅是读取敏感文件(如/etc/passwd)那么简单,更深层的危害在于它能与其它漏洞(如文件上传)结合,实现远程代码执行(RCE)。

关键在于“可控的输入点”。这个输入点可能来自$_GET$_POST$_COOKIE,甚至是$_SERVER中的某些变量(如HTTP_REFERER)。攻击者的目标就是通过这个输入点,让程序去包含一个它本不该包含的文件。理解这一点,就理解了所有绕过手法的出发点:无论过滤规则多么复杂,最终目的都是要构造一个能被目标系统正确解析、但又能指向我们恶意内容的“路径字符串”。

2.2 CTFshow题目环境常见设防分析

CTFshow的题目为了增加难度,通常会设置多层过滤。常见的防御姿势包括:

  1. 关键词过滤:过滤../(目录遍历)、php://(伪协议)、data://(数据流)、input等敏感字符串。可能使用str_replace()preg_match()waf进行拦截。
  2. 后缀限制:强制在包含路径后添加固定的后缀,如.php.txt。例如,用户输入file=test,实际代码执行的是include($_GET['file'] . '.php')
  3. 协议黑/白名单:只允许http://file://协议,或者明确禁止php://等。
  4. 目录限制:使用chdir()将当前目录切换到非Web目录,或者用basename()函数取文件名,试图剥离路径。

我们的绕过思路,就是针对这些过滤机制,利用PHP语言特性和系统环境特性,进行“变形”和“替代”。核心思路可以归纳为:当直接路径被禁,就用编码;当协议被禁,就找替代协议或利用已有文件;当后缀被追加,就用截断或利用协议特性忽略它。

3. 五种核心绕过姿势详解与实战

3.1 姿势一:利用PHP伪协议——php://filter的读取与编码绕过

这是最经典也是最常用的一招。php://filter协议本身用于在数据流打开时应用过滤器,但在文件包含中,它成了读取源码的利器。

核心利用:读取PHP文件源码当直接包含一个.php文件时,它会被服务器执行,我们看不到源代码。但通过php://filter,我们可以先让文件内容经过base64编码或rot13等转换,再输出,这样就能得到编码后的源码,解码即可。

?file=php://filter/read=convert.base64-encode/resource=index.php

实操要点

  • resource=后面跟的是你要读取的文件路径,可以是相对路径也可以是绝对路径。
  • convert.base64-encode是最常用的过滤器,因为输出结果规范,易于解码。string.rot13有时在特定过滤下也能用。
  • 得到的是base64编码字符串,需要在线或本地解码。

绕过过滤技巧: 如果题目过滤了php://或者filter等关键词,可以尝试以下变种:

  1. 大小写绕过PHP://FilterPhp://(部分环境下有效)。
  2. 多重编码绕过:对完整的payload进行URL编码一次或两次。例如,将php://filter/read=convert.base64-encode/resource=flag.php整体进行URL编码后传入。有时WAF只检查一次解码后的内容。
  3. 利用convert.iconv.*过滤器:这是一个更隐蔽的过滤器,可以用于字符集转换,在某些情况下也能用于读取文件,但不如base64通用。例如convert.iconv.UTF-8.UTF-16LE,输出会是另一种格式的“乱码”,需要对应解码。

注意:php://filterwrite过滤器在仅包含场景下通常无法直接用于写文件,它主要用于读取。写文件需要php://input结合特定条件。

3.2 姿势二:数据流伪装——php://input与POST组合拳

当目标服务器允许包含php://input流,并且allow_url_include配置为On时,我们可以直接执行POST过去的PHP代码。

利用方法

GET: ?file=php://input POST Body: <?php system('ls');?>

原理php://input是一个只读流,可以访问请求的原始数据。当我们包含它时,POST过去的数据会被当作PHP代码执行。

关键条件与绕过

  1. allow_url_include=On:这是大前提。在CTF题目中常为开启状态,但实际生产环境极少开启。
  2. 过滤php://input:如果被过滤,同样可以尝试大小写、双重URL编码。有时甚至可以用PHP://INPUT
  3. data://协议作为备选:如果php://input被禁,但allow_url_include为On且data://未被过滤,可以使用data://text/plain,<?php phpinfo();?>data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+(base64编码后的代码)。data://协议同样将后续内容作为输入流包含并执行。

实操心得: 使用php://input时,务必使用POST方法传参,并且请求头Content-Type不一定需要是application/x-www-form-urlencoded,纯文本即可。Burp Suite中直接写在Raw里发送很方便。如果执行命令没回显,可以尝试将输出写入一个文件,或者使用curl外带数据到自己的服务器。

3.3 姿势三:日志投毒与包含——利用系统固有文件

这是非常巧妙且实用的一招,尤其在无法上传文件但可以包含日志文件的情况下。Web服务器(如Apache、Nginx)的访问日志会记录每一个请求,包括请求行、头部。如果我们把PHP代码作为请求的一部分发送,这段代码就会被原样记录到日志文件中。然后,我们再利用文件包含漏洞去包含这个日志文件,其中的PHP代码就会被执行。

标准步骤

  1. 定位日志路径:常见路径如/var/log/apache2/access.log/var/www/logs/access_log/usr/local/nginx/logs/access.log等。可以通过报错信息、读取/proc/self/environ/etc/apache2/apache2.conf等配置文件来猜测。
  2. 投毒:向网站发送一个包含PHP代码的请求。例如,在User-Agent中插入代码:User-Agent: <?php @eval($_POST['cmd']);?>。因为UA头会被记录到日志。
  3. 包含日志:使用文件包含漏洞,包含已知路径的访问日志文件,如?file=/var/log/apache2/access.log
  4. 执行代码:包含成功后,日志文件中的PHP标签会被解析,此时我们就可以通过POST传递cmd参数来执行命令了。

绕过与技巧

  • 日志文件过大:如果日志文件太大,包含可能导致超时或内存不足。可以尝试在投毒后立即进行包含,或者利用工具频繁请求产生新日志行。
  • 路径未知:需要结合目录遍历或信息泄露漏洞来获取路径。有时默认路径就可行。
  • 编码问题:如果日志对特殊字符进行了转义,可能导致代码无法执行。可以尝试将PHP代码进行Base64编码,然后在投毒时使用<?php eval(base64_decode('...'));?>的方式。或者使用短标签<?= system('ls') ?>,它更短,被转义破坏的可能性稍低。
  • 利用其他日志:除了访问日志,错误日志(error.log)也可能被包含,有时错误信息中会包含部分路径,有助于定位。

这个姿势的魅力在于,它利用了系统固有的、看似无害的文件,将文件包含漏洞的利用从“需要上传文件”提升到了“只需发起请求”的维度。

3.4 姿势四:编码与截断的艺术——应对后缀限制

当题目给用户输入自动添加后缀时,例如include($_GET['file'] . '.php'),我们需要想办法“截断”这个后缀,或者让它失效。

1. URL编码与双重编码这是最基本的绕过。服务器在接收到参数后,通常会进行一次URL解码。如果WAF在解码前检查,我们可以对敏感字符进行编码。

  • 过滤../?尝试使用..%2f/的URL编码)或%2e%2e%2f./的编码)。
  • 在Windows环境下,还可以用..\(反斜杠)或它的URL编码..%5c
  • 双重编码:如果服务器进行了两次URL解码,那么我们可以对payload编码两次。例如,../->%2e%2e%2f(一次编码) ->%252e%252e%252f(二次编码,%被编码为%25)。这经常能绕过简单的字符串匹配。

2. 长度截断(PHP版本 < 5.3.4)这是一个历史漏洞,但在老环境或特定CTF题目中可能出现。原理是PHP在包含文件时,如果路径长度超过一定限制(通常是4096字节),超出的部分会被截断。我们可以构造很长的路径,让系统添加的后缀.php被挤在4096字节之后,从而被截断丢弃。

?file=test.txt/././././...(重复非常多次)./././

实操要点:这种方法依赖特定PHP版本和环境,现在较少见,但需要了解。

3. 零字节截断(PHP版本 < 5.3.4,magic_quotes_gpc=Off)同样是历史漏洞,但非常经典。在C语言中,字符串以空字符(\0,URL编码为%00)作为结束符。早期PHP在处理包含路径时,如果遇到%00,会认为字符串到此结束,后面的内容被忽略。

?file=../../../../etc/passwd%00

这样,即使代码是include($file . '.php'),实际拼接后变成../../../../etc/passwd%00.php,PHP在读取时遇到%00就停止了,成功包含/etc/passwd

注意:这两个截断方法在现代PHP环境中已修复,仅在特定复古场景或CTF中可能遇到。它们提醒我们,环境版本信息是渗透测试中极其重要的一环。

3.5 姿势五:协议替换与路径遍历的奇技淫巧

当常见的协议和路径遍历符号被严格过滤时,我们需要一些“偏门”的协议和技巧。

1. 利用file://协议和绝对路径如果php://被禁,但file://协议未被禁(或默认允许),可以直接使用绝对路径。file://是本地文件传输协议。

?file=file:///etc/passwd ?file=file:///var/www/html/config.php

绕过点:即使过滤了../,只要你知道目标的绝对路径,依然可以直接包含。获取绝对路径可以通过报错信息、读取/proc/self/cwd(当前进程工作目录)等符号链接来实现。

2. 利用phar://协议(PHP Archive)phar://协议用于访问PHAR(PHP归档)文件内的内容。一个精妙的利用点是:PHAR的元数据(metadata)在反序列化时会自动触发__wakeup()__destruct()等魔术方法。这意味着,我们可以构造一个包含恶意序列化对象的PHAR文件,通过文件包含(或其它能触发反序列化的点)来触发漏洞,实现RCE。这在文件上传+文件包含的组合场景中潜力巨大。简要利用步骤

  1. 编写一个包含恶意类的PHP脚本,并生成一个包含该类序列化对象的PHAR文件。
  2. 将PHAR文件上传到服务器(后缀可能是.jpg,通过文件上传漏洞)。
  3. 通过文件包含漏洞包含这个PHAR文件:?file=phar://./uploads/evil.jpg/内部文件路径。即使包含时指定了内部文件,在解析phar时,元数据反序列化过程依然会被触发。

3. 利用zip://协议phar://类似,zip://可以访问ZIP压缩包中的文件。我们可以将一个PHP木马压缩成ZIP,然后上传,再通过包含ZIP包内的文件来执行代码。关键点zip://协议需要指定压缩包内的具体文件路径,并且路径中要用#号,且#需要URL编码为%23

# 假设上传了 shell.zip,里面有一个 shell.php 文件 ?file=zip://./uploads/shell.zip%23shell.php

4. 目录遍历的变形

  • 绝对路径跳转:如果当前在/var/www/html,想包含根目录下的文件,直接用/etc/passwd(绝对路径)可能比../../../../etc/passwd(相对路径)更有效,尤其当../被过滤时。
  • 利用软链接:如果服务器上存在一些指向敏感目录的软链接,包含它们可能达到目的。例如,某些环境下的/proc/self/fd/目录下的文件描述符可能指向有趣的文件。
  • 非预期文件包含:包含/dev/fd/0(标准输入)或/proc/self/environ(环境变量),如果其中存在可控数据(如User-Agent在environ中),也可能造成代码执行,但条件较为苛刻。

4. 实战场景串联与综合绕过案例

假设我们遇到一道综合题,过滤规则如下:

  1. 过滤了php://data://phar://等字符串。
  2. 过滤了../..\
  3. 自动为输入添加.php后缀。
  4. 无法直接上传文件。

我们的攻击链可以这样设计:第一步:信息收集。尝试读取index.php源码,看看过滤逻辑。使用php://filter被禁,尝试大小写PHP://或双重URL编码。如果不行,尝试包含/proc/self/cmdline查看进程启动命令,或者包含/etc/nginx/nginx.conf等配置文件,寻找日志路径、Web根目录等线索。第二步:寻找替代包含点。如果找到了日志路径(如/var/log/nginx/access.log),尝试进行日志投毒。将一句话木马写入User-Agent,然后包含该日志文件。第三步:利用编码绕过后缀。如果包含日志时,代码是include($file . '.php'),我们传入/var/log/nginx/access.log%00(如果环境支持零字节截断),或者传入一个超长路径的日志路径(长度截断)。如果都不行,考虑是否日志文件本身有.log后缀,包含后拼接.php变成access.log.php,这个文件不存在,会导致包含失败。这时需要思考,是否有可能让日志文件本身被解析为PHP?如果服务器配置错误,将.log文件也交给PHP解析,那么包含日志文件本身就能执行代码,无需截断后缀。但这属于配置层面,非本题考察重点。第四步:获取Shell。通过日志包含成功执行代码后,使用systempassthru函数执行命令,查找flag文件。或者,如果允许,直接写入一个Webshell到可写目录。

这个案例展示了如何将多种姿势串联起来,根据实际情况灵活应对。核心是:永远不要只依赖一种方法,当一条路被堵死,立即根据已有信息寻找下一条路。

5. 防御视角与排查技巧

理解了攻击,才能更好地防御。从开发和安全运维角度,以下措施至关重要:

1. 严格的输入校验

  • 白名单机制:这是最有效的方法。如果只需要包含固定的几个文件,直接使用白名单。
  • 路径固定:避免将用户输入直接拼接进包含路径。如果需要动态包含,应基于一个固定的基础目录,然后只允许用户输入文件名(不含路径),并使用basename()函数确保只有文件名部分。
    // 错误示例 include($_GET['page'] . '.php'); // 改进示例(仍不完美,但稍好) $page = basename($_GET['page']); // 剥离路径 $allowed_pages = ['home', 'about', 'contact']; if (in_array($page, $allowed_pages)) { include('./pages/' . $page . '.php'); } else { include('./pages/404.php'); }

2. 关闭危险配置

  • php.ini中,确保allow_url_fopen = Offallow_url_include = Off。这是防止远程文件包含(RFI)和php://inputdata://等协议被利用的底线。
  • 确保magic_quotes_gpc已弃用且关闭(现代PHP版本默认如此),但不要依赖它作为安全手段。

3. 安全编程习惯

  • 使用require_onceinclude_once代替require/include,避免重复包含可能导致的问题(虽然对漏洞本身无直接影响)。
  • 对动态包含的文件路径进行正则匹配,严格限制字符集(如只允许字母、数字、下划线、短横线)。
  • 考虑使用安全的文件映射或路由机制,完全避免动态包含。

4. 运维层面

  • 定期进行代码审计,特别是检查所有包含函数(include,require,include_once,require_once,file_get_contents等)的参数是否用户可控。
  • 配置Web服务器(如Nginx)的日志权限,确保日志目录不可被Web用户执行。
  • 保持PHP、Web服务器等中间件版本更新,避免已知的截断类漏洞。

排查技巧实录: 在内部代码审计或渗透测试中,如何快速定位潜在的包含漏洞?

  1. 全局搜索:在项目代码中全局搜索includerequireinclude_oncerequire_oncefile_get_contentsfopen等函数。
  2. 追踪变量:检查这些函数的参数是否是变量,并向上追踪该变量的来源,是否来自$_GET$_POST$_COOKIE$_REQUEST$_SERVER中的某些字段(如$_SERVER['QUERY_STRING']$_SERVER['PHP_SELF'],这些也可能被污染)。
  3. 检查过滤:查看对用户输入是否有过滤,过滤逻辑是否严谨,是否存在被绕过的可能(如大小写、编码、截断)。
  4. 模拟测试:在测试环境,尝试使用各种payload(伪协议、路径遍历、编码)进行测试,观察响应。注意查看错误日志,有时错误信息会泄露路径。

文件包含漏洞的攻防是一场关于“输入控制”的博弈。攻击方绞尽脑汁让输入“变形”以通过检查,防御方则需在每一个环节设卡,最小化攻击面。通过这次对五种绕过姿势的深度剖析,我希望你收获的不只是几个payload,而是那种面对黑盒过滤时,如何层层拆解、灵活组合的思维方式。在实际工作中,无论是攻击测试还是防御建设,这种思维方式都比单纯记忆漏洞利用代码要重要得多。最后一个小建议:搭建一个带有各种过滤规则的靶场环境,自己亲手去尝试绕过,遇到问题再去查阅资料,这个过程带来的理解是最深刻的。