1. 项目概述:一次典型的PHP文件包含漏洞实战

最近在复盘一些经典的Web安全实战场景,发现PHP文件包含漏洞(File Inclusion Vulnerability)依然是许多CTF比赛和渗透测试中的高频考点,也是理解Web应用安全逻辑的一个绝佳切入点。这次我们就来深度拆解一个名为“巧用PHP文件包含漏洞获取flag.php”的实战项目。这个标题听起来就很直接,目标明确:利用一个存在文件包含漏洞的PHP应用,最终读取到服务器上名为 flag.php 的关键文件。 flag 通常是CTF比赛中的“旗帜”,里面藏着通关所需的密钥或答案,而在真实渗透测试中,它可能代表着敏感配置文件、数据库凭证或其他关键信息。

对于刚接触Web安全的朋友来说,文件包含漏洞可能有点抽象。简单类比一下,它就像是一个本应只允许你查看“公共书架”上指定书籍的图书馆管理员(Web应用),但因为管理员的指令(代码逻辑)有缺陷,你不仅能让他拿公共书架的书,还能通过一些特殊的“暗号”(比如路径遍历或伪协议),让他去拿“馆长办公室保险柜”(服务器其他目录)里的机密文件,甚至让他去执行你递给他的一张写着指令的纸条(远程文件包含执行代码)。这个漏洞的危害性极大,因为它可能直接导致敏感信息泄露、网站被挂马,甚至整个服务器被控制。

本篇文章,我将以一个模拟的实战环境为背景,带你一步步拆解如何发现、确认并最终利用一个PHP本地文件包含(LFI)漏洞,并进阶到利用PHP伪协议来读取源代码,最终拿到 flag.php 中的内容。整个过程,我会穿插讲解背后的原理、每一步的操作意图、可能遇到的坑以及我个人的一些排查技巧。无论你是正在学习安全的学生,还是希望加固自己应用的开发者,相信都能从中获得实实在在的收获。

2. 漏洞原理与核心环境搭建

2.1 PHP文件包含漏洞的本质

要利用漏洞,首先得明白它为什么会产生。PHP中提供了四个用于文件包含的函数: include require include_once require_once 。它们的本意是提高代码复用性,比如把数据库连接配置、页头页尾等公共部分写成单独的文件,然后在需要的地方包含进来。漏洞产生的根本原因在于,开发者 将用户可控的输入,未经充分过滤就直接拼接到了这些包含函数的参数中

举个例子,一个典型的漏洞代码可能长这样:

<?php
$page = $_GET['page']; // 直接接收用户输入的参数
include($page . '.php'); // 拼接后缀后直接包含
?>

这段代码的初衷可能是让用户通过 ?page=home 来访问 home.php 页面。但如果攻击者传入 ?page=../../../../etc/passwd ,经过拼接后,服务器尝试包含的文件就变成了 ../../../../etc/passwd.php 。如果服务器没有对路径进行限制,攻击者通过 ../ 进行目录遍历,就可能读取到系统敏感文件。更危险的是,如果应用在部署时错误配置,没有强制添加 .php 后缀,或者攻击者利用空字节截断(在PHP旧版本中)等方式,就能直接包含 /etc/passwd 这样的系统文件。

本地文件包含(LFI)和远程文件包含(RFI)是它的两种主要形式。LFI只能包含服务器本地的文件,而RFI则可以通过 http:// ftp:// 等协议包含远程服务器上的文件,这通常意味着可以直接执行远程的恶意代码,危害等级更高。现代PHP默认配置通常关闭了 allow_url_include 选项,因此RFI较少见,但LFI依然广泛存在且威力不容小觑。

2.2 靶场环境搭建与初步探测

为了实战,我们需要一个存在漏洞的环境。这里我推荐使用DVWA(Damn Vulnerable Web Application)或专门的文件包含漏洞靶场。以DVWA为例,将其部署在本地(例如使用PHPStudy、XAMPP或Docker),并将安全级别设置为“Low”。

访问文件包含漏洞模块,我们通常会看到一个简单的页面,可能有一个文件选择器或链接,URL类似 http://target/vuln.php?page=file1.php 。我们的第一步就是信息收集:

  1. 参数探测 :尝试修改 page 参数的值,观察页面变化。例如,尝试 ?page=file2 ,看是否会自动添加后缀;尝试 ?page=../../../../etc/passwd ,看是否会报错或输出系统信息。
  2. 错误信息分析 :PHP的错误信息是宝藏。如果开启了 display_errors ,错误的文件路径会直接暴露在页面上,这能帮助我们确认漏洞是否存在,并了解服务器的部分目录结构。
  3. 后缀测试 :尝试不带后缀、带不同后缀( .php , .inc , .txt , .log )的输入,观察服务端的处理逻辑。

在我的这次实战模拟中,目标URL是 http://192.168.1.100/vuln.php?page=include.php 。将参数改为 ?page=../../../../etc/passwd 后,页面返回了一个Warning,提示 include.php 文件不存在,但并未显示遍历路径的错误。这暗示了服务端可能对输入添加了固定的后缀(如 .php ),导致我们传入的路径被拼接后变成了一个不存在的 .php 文件。这是一个重要的线索,意味着我们需要找到方法“绕过”这个后缀限制。

注意 :在实际测试中,务必在授权范围内进行。未经授权的测试是违法的。使用本地靶场或获得明确授权的演练环境是唯一正确的方式。

3. 核心利用链:从LFI到源码读取

3.1 利用PHP伪协议绕过限制

当直接路径遍历读取 /etc/passwd 失败时,我们就要祭出PHP文件包含漏洞的“神器”——PHP伪协议(PHP Wrappers)。伪协议允许我们以流的方式访问各种资源,在文件包含的语境下,最常用的是 php://filter php://input

php://filter 协议 是我们读取PHP文件源代码的关键。因为正常情况下,服务器包含一个 .php 文件时会执行它,我们看不到源代码。但 php://filter 可以让我们在包含文件前,先对文件流进行一层“过滤”处理,比如将其内容进行Base64编码。这样,服务器执行的不是PHP代码,而是经过编码后的文本,我们拿到编码文本后再解码,就能获得源代码。

其基本格式为: php://filter/convert.base64-encode/resource=目标文件路径

现在,结合我们之前发现的“自动添加 .php 后缀”的情况,构造Payload: ?page=php://filter/convert.base64-encode/resource=flag 。服务器接收到这个参数后,会拼接成 php://filter/convert.base64-encode/resource=flag.php ,然后去包含这个“资源”。 resource= 后面的 flag 会被当作文件名,并加上 .php 后缀,最终指向我们想读取的 flag.php 文件。 convert.base64-encode 过滤器会将这个文件的内容进行Base64编码后再输出。

发送这个Payload后,页面没有显示正常的网页内容,而是输出了一串看似乱码的Base64字符串,例如 PD9waHAgCiRmbGFnID0gImN0ZntUaGlzX0lTX0FfRkxBR30iOwpAZW5kPw== 。这是一个强烈的成功信号!

3.2 解码与Flag提取

拿到Base64编码的字符串后,我们需要将其解码。可以在本地使用Python、在线解码工具或Burp Suite的Decoder模块进行解码。将上面的字符串解码后,我们得到:

<?php
$flag = "ctf{This_IS_A_FLAG}";
@end?>

Bingo!我们成功读取到了 flag.php 的源代码,并从中获取到了Flag: ctf{This_IS_A_FLAG} 。这个过程清晰地展示了如何将一次看似被后缀限制的本地文件包含,通过伪协议转化为源代码读取漏洞。

这里有一个关键技巧 :为什么用Base64编码?因为PHP文件可能包含各种特殊字符(如 < ? > ),直接输出可能会被浏览器解释为HTML标签或触发其他处理,导致显示不全或错乱。Base64编码能将任何二进制数据转化为纯ASCII字符,确保内容完整、无损地传输。除了 base64-encode read string.rot13 等过滤器也可能在特定场景下有用,但Base64是最通用可靠的。

4. 漏洞利用的深入与扩展

4.1 利用日志文件进行RCE(条件苛刻但值得了解)

如果目标服务器不仅存在LFI,我们还能找到写入特定内容的日志文件,就有可能将LFI升级为远程代码执行(RCE)。这是一种更高级的技巧,成功率依赖于环境配置。

常见利用点

  1. Web访问日志 :如Apache的 /var/log/apache2/access.log 或Nginx的 /var/log/nginx/access.log 。当我们访问网站时,浏览器发送的HTTP请求头(如 User-Agent )会被记录到日志中。如果我们能控制 User-Agent ,并将其设置为一段PHP代码(如 <?php system($_GET[‘c’]);?> ),那么这段代码就会被原样写入日志文件。
  2. 其他用户输入日志 :如一些应用的自定义错误日志,可能记录了包含未过滤参数的URL。

利用步骤

  1. 确认日志路径 :通过LFI尝试包含常见的日志路径,或根据错误信息推测。
  2. 污染日志 :使用Burp Suite或Curl,发送一个特殊的HTTP请求,将 User-Agent 设置为PHP代码。
    curl -H “User-Agent: <?php system(\$_GET[‘cmd’]);?>” http://target/
    
  3. 包含日志文件 :通过LFI漏洞去包含这个日志文件,例如 ?page=../../../var/log/apache2/access.log 。由于日志文件中包含了我们注入的PHP代码,服务器在包含时会将其当作PHP代码执行。
  4. 执行命令 :现在,我们可以在包含日志文件的URL后面附加参数来执行命令,例如 ?page=../../../var/log/apache2/access.log&cmd=id ,服务器就会执行 id 命令并将结果返回。

重要注意事项 :这种方法在实际中面临诸多挑战。现代服务器日志可能对特殊字符进行转义,PHP代码无法原样写入;日志文件权限可能不允许Web用户读取;日志文件体积巨大,包含可能导致超时或内存耗尽。因此,这通常作为在非常理想化的靶场环境或老旧系统上的拓展思路,而非首选方案。

4.2 利用PHP临时文件与包含

另一种更隐秘的思路涉及PHP处理文件上传时产生的临时文件。当用户上传文件时,PHP会先将文件保存到一个临时的位置(如 /tmp/phpXXXXXX ),然后在脚本执行结束后删除。如果存在LFI漏洞,且我们能精确预测或爆破出这个临时文件名(时间窗口极短),理论上可以包含它。但这需要精确的时间竞争攻击(Race Condition),难度极高,在实际渗透中几乎不可行,仅在部分CTF极端题目中出现。

5. 防御策略与安全开发建议

作为开发者,了解漏洞如何利用,是为了更好地防御它。以下是一些根本性的防御措施:

  1. 白名单校验 :这是最有效的方法。不要使用用户输入直接作为包含的文件名。应该预先定义好允许包含的文件列表(白名单),用户输入只能选择白名单中的项。

    $allowed_pages = [‘home.php’, ‘about.php’, ‘contact.php’];
    $page = $_GET[‘page’];
    if (in_array($page, $allowed_pages)) {
        include($page);
    } else {
        include(‘error.php’);
    }
    
  2. 固定目录与后缀 :如果必须动态包含,应将包含文件限制在某个特定目录下,并强制添加后缀。

    $base_dir = ‘/var/www/html/includes/’;
    $page = basename($_GET[‘page’]); // 使用basename去除路径
    $file = $base_dir . $page . ‘.php’;
    if (file_exists($file) && is_file($file)) {
        include($file);
    }
    
  3. 避免动态包含 :重新审视架构,是否真的需要动态文件包含?很多情况下,使用路由分发(如 index.php?action=home 然后根据action调用对应函数或类)是更安全的选择。

  4. 安全配置

    • 在生产环境中关闭 display_errors ,防止路径信息泄露。
    • open_basedir 配置项设置为Web根目录,限制PHP可访问的文件系统范围。
    • 确保 allow_url_include 设置为 Off (默认值),彻底杜绝RFI的可能性。
  5. 代码审计与自动化扫描 :在开发流程中引入安全代码审计和自动化漏洞扫描工具,可以有效识别出潜在的文件包含漏洞点。

6. 实战排查与常见问题记录

在实际利用过程中,你可能会遇到各种各样的问题。下面是我总结的一些常见场景和排查思路:

问题现象 可能原因 排查与尝试方向
包含路径后返回空白页 1. 文件不存在。
2. 文件存在但无打印输出。
3. 包含的文件中有语法错误,且错误显示被关闭。
1. 检查路径是否正确,尝试包含一个已知存在的文件(如 index.php )。
2. 使用 php://filter 读取源码,看文件内容是否为空或有逻辑问题。
3. 开启靶场的错误显示,或查看服务器错误日志。
使用 php://filter 仍被添加后缀 服务端过滤逻辑可能将整个参数字符串拼接后缀,伪协议被破坏。 尝试使用 ?page=php://filter/convert.base64-encode/resource=flag.php%00 (空字节截断,仅对PHP<5.3.4有效)。或尝试其他伪协议如 zip:// phar:// (需配合文件上传)。
包含日志文件无反应 1. 日志路径错误。
2. 日志文件权限不足(Web用户无法读取)。
3. 注入的代码被转义或过滤。
1. 通过LFI包含 /proc/self/cwd/ 等来推断当前工作目录,再推测日志路径。
2. 尝试包含 /etc/passwd 等确认基本LFI是否有效,以排除权限问题。
3. 查看日志文件内容(如果可读),确认代码是否被原样写入。注意 <?php ?> 标签可能被部分WAF过滤。
伪协议被禁用 服务器可能通过配置或WAF禁用了部分伪协议。 尝试其他伪协议,如 file:// http:// (如果allow_url_include开启)、 data:// 。或者回归到传统的路径遍历,尝试读取 /proc/self/environ (环境变量)或 /proc/self/fd/ (文件描述符)等特殊文件。

个人心得 :在利用LFI时,思维一定要灵活。不要只盯着 ../../etc/passwd 。多观察页面的反馈,一个Warning信息、一个异常的报错都可能成为突破口。善用Burp Suite的Intruder模块进行路径或参数的模糊测试(Fuzzing),可以帮助你快速发现可读的目录和文件。记住,信息收集永远是渗透测试中最重要的一环,有时 flag 并不直接藏在 flag.php 里,而是需要通过包含其他配置文件(如 config.php dbconn.php )找到数据库凭证,进而从数据库中提取出来。

最后,对于学习者而言,手动搭建漏洞环境、亲手构造Payload、观察每一步的输入输出,远比只看文章收获大。理解漏洞的原理和防御之道,不仅能让你在CTF中游刃有余,更能帮助你在开发中写出更健壮、更安全的代码。安全是一个攻防对抗、持续学习的过程,每一次实战解析都是为了筑起更牢固的防线。

更多推荐