文件包含漏洞(PHP)
1. 漏洞产生原理
- PHP 中文件包含函数有:
require()
require_once()
include()
include_once()
include
和require
的区别就是,include 在包含的过程中若出现错误,就会抛出一个警告,程序继续执行;但是require
函数出现错误的时候,会直接报错并且退出程序的执行。_once()
表示函数只包含一次,用于避免函数重定义、变量重新赋值等问题。- 这四个函数会将包含的文件一律当成 php 文件处理,因此被包含的文件只要有 php 的代码,其就会被执行。
2. 与文件包含漏洞常配合的伪协议,伪协议的内容会回显
1. php://
输入输出流
PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。
1. php://filter
参数表格:
名称 描述 resource=<要过滤的数据流>
这个参数是必须的。它指定了你要筛选过滤的数据流。 read=<读链的筛选列表>
该参数可选。可以设定一个或多个过滤器名称,以管道符(` write=<写链的筛选列表>
该参数可选。可以设定一个或多个过滤器名称,以管道符(` <;两个链的筛选列表>
任何没有以 read=
或write=
作前缀 的筛选器列表会视情况应用于读或写链。常见的用法
php://filter/read=convert.base64-encode/resource=URL
将文件的内容用 base64 编码。这里的
convert.base64-encode
是一个过滤器,还有其他的过滤器可以使用,详见:https://www.php.net/manual/zh/filters.php。如果某些过滤器被过滤了,就用其他的过滤器。
filter 是可以写入的:
php://filter/write=convert.base64-decode/resource=webshell.php&txt=base64后的内容
2. php://input
,使用时需要 allow_url_include = open
php://input
是一个只读信息流,当请求方法是 POST 且enctype 不为 multipart/form-data
时,其可以获取原始请求的数据。- 其可以绕过
file_get_contents()
的某些限制,具体是:file_get_contents()
函数将文件内容读入到一个字符串中(参数为文件的路径)。- 而通过
php://input
伪协议从 POST 主体中读取到的内容会是一个流,同时也是一个原始数据。 - 如果
file_get_contents()
函数内读取的文件是通过php://input
这个伪协议,那么其就会将获取到这个原始数据流 - 相当于
php://input
的内容被直接传到了file_get_contents()
内并生成一个字符串,这样就绕过了file_get_contents()
必须读取 URI 文件的限制。 - 大部分情况下这两个混在一起使用是为了文件上传的用途,是正常业务。其将 POST 中的内容直接当作文件流传入到
file_get_contents()
。(注意 POST 不用加参数)。
- 如果
url_include = open
,此时php://input
用于文件包含时,其通过 POST 参数传入的内容可以执行 php 代码(相当于传入了 php 文件流),从而可以写入木马或者产生命令执行漏洞。
2. file://
伪协议,不受 allow_url_fopen 和 allow_url_include
的影响
- 被文件包含时,可以直接读取文件,由于文件包含的特性,都会被当成 php 文件执行。
- 路径可以是盘符路径,或者是 http 协议等。
3. data://
伪协议,需要 allow_url_fopen 和 allow_url_include 都为 on
(条件有点苛刻)
其为数据流封装器,类似于
php://input
和 格式封装器的结合。当被文件包含时,常见用法:
data://text/plain, php 代码
data://text/plain; base64, base64 加密后的 php 代码
这样传入的内容就会被当成 php 代码执行。
相比
php://input
,其只需要在 url 中输入即可,但是使用条件较为苛刻。
4. zip:// 和 compress.bzip2:// 和 compress.zlib://
三个伪协议
这三个为压缩流协议,可以将文件解析成压缩流。
利用:
将含有 php 代码的文件(txt 或其他)压缩成
zip、bz2、gz(和标题的三个对应)
。然后用这三个流读取,这样先会把他们解压并变成文件流。然后配合文件包含漏洞,里面的内容当成 php 代码执行。
5. phar://
同样可以访问 zip 格式压缩包内容。示例:
file=phar://文件路径
// TODO:利用 phar 拓展 php 反序列化漏洞
6. http 协议,需要 allow_url_open 和 allow_url_include 都为 on
用于远程文件包含漏洞(RFI)
// TODO:对于 RFI,如果条件都为 off,还可以使用 SMB 来绕过。
3. 和临时文件结合从而 GetShell
参考:
https://qftm.github.io/2020/03/15/LFI-PHPINFO-OR-PHP7-Segment-Fault/#toc-heading-19
https://www.cnblogs.com/linuxsec/articles/11278477.html在 PHP 中可以使用 POST 方法或者 PUT 方法进行文本和二进制文件的上传。上传的文件信息会保存在全局变量
$_FILES
里。
$_FILES
超级全局变量很特殊,他是预定义超级全局数组中唯一的二维数组。其作用是存储各种与上传文件有关的信息,这些信息对于通过 PHP 脚本上传到服务器的文件至关重要。
$_FILES['userfile']['name']
这个变量值的获取很重要,因为临时文件的名字都是由随机函数生成的,只有知道文件的名字才能正确的去包含它。文件被上传后,默认会被存储到服务端的默认临时目录中,该临时目录由 php.ini 的
upload_tmp_dir
属性指定,假如upload_tmp_dir
的路径不可写,PHP 会上传到系统默认的临时目录中。
不同系统服务器常见的临时文件默认存储目录,了解系统的默认存储路径很重要,因为在很多时候服务器都是按照默认设置来运行的。同时临时文件的文件名都是随机生成的。
3.1 PHP7 Segment Fault
利用版本:7.0.0 <= PHP Version < 7.0.28。
所谓的段错误(segment fault)就是指访问的内存超过了系统所给这个程序的内存空间。从而发生程序退出。缓存文件就留在了 tmp 目录。
详细来讲,在含有文件包含漏洞的地方,使用php://filter/string.strip_tags
导致 php 崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个 tmp file 就会一直留在 tmp 目录。例题:[NPUCTF2020]ezinclude1。
攻击脚本:
1
2
3
4
5
6
7
8import requests
from io import BytesIO
payload = "<?php phpinfo();?>"
# 将 Payload 当作文件上传
upload_data = {'file': BytesIO(payload.encode())}
url = "http://00eb061e-e914-464c-8b53-2855783dd4f4.node5.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=./index.php"
r = requests.post(url=url, files=upload_data, allow_redirects=False)CTF 题会给方法,以便查看 tmp 目录下的文件名,如果没有,那就只能爆破了。
4. 绕过 require_once()
的一次包含的限制
参考:
原理涉及 PHP 底层源码和 Linux 进程管理,太难了看不懂,PoC 拿来用就行:
1
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php