在上一篇文件上传的内容中笔者阐述了文件上传漏洞产生的相关原理以及使用了一个pikachu靶场的例子进行演示,在这个例子中涉及到了前端代码对于文件上传漏洞的相关防护,以及站在攻击者的角度我们要如何绕过前端的防护成功进行攻击;但是事实上对于文件上传漏洞相关的安全防护不仅在前端能够部署,在后端也能够部署,那么本文我们就来探讨一下在后端的相关过滤措施有哪些以及站在攻击者的角度我们如何进行绕过。
1.MIME类型过滤:
MIME:
MIME(Multipurpose Internet Mail Extensions)是一种互联网标准,用于描述电子邮件、Web等互联网应用中传输的数据类型。MIME类型最初是为了解决电子邮件中传输多媒体内容的问题,现在已经广泛用于HTTP协议中,用于描述和处理多种类型的网络资源。
MIME类型的结构
MIME类型由两部分组成,格式为type/subtype
:
-
type:主要类型,表示数据的大类,例如
text
、image
、audio
、video
、application
等。 -
subtype:具体子类型,表示数据的具体格式,例如
plain
、html
、jpeg
、png
、mpeg
、json
等。
例如:
text/html 表示HTML文档 image/jpeg 表示JPEG图像 application/json 表示JSON数据
言归正传,文件上传MIME类型过滤是指在文件上传功能中,通过检查上传文件的MIME类型来限制允许上传的文件类型。这可以防止用户上传恶意文件,增强应用程序的安全性。以下是一些关于如何实现文件上传MIME类型过滤的详细信息和示例。
相关源码:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
$fileType = mime_content_type($_FILES['file']['tmp_name']);
if (!in_array($fileType, $allowedTypes)) {
die('Invalid file type.');
}
// 处理文件上传逻辑,例如移动文件到目标目录
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . basename($_FILES['file']['name']));
echo 'File uploaded successfully.';
}
?>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
定义一个数组,包含允许上传的文件MIME类型。
$fileType = mime_content_type($_FILES['file']['tmp_name']);
使用mime_content_type
函数获取上传文件的MIME类型。$_FILES['file']['tmp_name']
是上传文件的临时文件路径。
if (!in_array($fileType, $allowedTypes)) { die('Invalid file type.'); }
-
使用
in_array
函数检查上传文件的MIME类型是否在允许的类型列表中。如果不在,则终止脚本执行并输出"Invalid file type."。
绕过方式:
通过BP抓包,通过修改请求头中的Content-Type字段进行绕过;当我们上传的文件为php文件时,该字段的内容如下图:
这个时候我们可以将该字段修改为如下示例MIME类型进行绕过:
image/jpeg, image/png, application/pdf,text/plain,text/html,image/png
以下是一个包含常见MIME类型的表格(10行):
文件扩展名 | MIME类型 | 描述 |
---|---|---|
.html | text/html | HTML文档 |
.css | text/css | CSS样式表 |
.js | text/javascript | JavaScript代码 |
.json | application/json | JSON格式 |
.jpg | image/jpeg | JPEG图像 |
.png | image/png | PNG图像 |
.gif | image/gif | GIF图像 |
.mp3 | audio/mpeg | MP3音频 |
.mp4 | video/mp4 | MP4视频 |
application/pdf | PDF文档 |
当然可修改的值不止这些,我们完全可以将各种MIME类型存入txt文件中,到时候再使用BP进行FUZZ测试即可。
2.扩展名验证:
①黑名单:
使用扩展名黑名单是防止文件上传漏洞的一种有效方法。通过禁止上传包含特定扩展名的文件,可以减少恶意文件被上传到服务器的风险。以下是一些常见的恶意文件扩展名,可以添加到黑名单中;相关代码(upload靶场为例):
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
代码逻辑:
$deny_ext = array('.asp','.aspx','.php','.jsp');
定义一个数组,包含不允许上传的文件扩展名。
接着通过对上传文件名进行处理,获取上传文件的扩展名;紧接着!in_array($file_ext, $deny_ext)
判断当前上传文件扩展名是否再黑名单列表中,若在则禁止上传(不将文件进行保存)。
此时的绕过思路:
使用特殊文件后缀进行绕过,以php为例子:
①.php3, .php4, .php5, .php7:用于指定 PHP 版本的文件后缀,较少使用。 ②.phtml:表示包含 PHP 代码的 HTML 文件,通常用于混合 HTML 和 PHP 代码。 ③.inc:通常表示包含文件(include file),虽然本身不是 PHP 后缀,但常包含 PHP 代码并通过 include 或 require 引入其他 PHP 文件。(需要配合文件包含漏洞)
因为上述代码中的黑名单列表并不完善所以这个时候我们只需要将php文件后缀进行修改上传即可。如果是自身在进行开发时需要将黑名单列表尽可能地完善,一下时常见的黑名单列表:
.php, .php3, .php4, .php5, .phtml, .phps, .asp, .aspx, .jsp, .jspx, .cfm, .cgi, .pl, .py, .rb, .sh, .bat, .exe, .com, .cmd, .dll, .scr, .msi, .vbs, .js, .jse, .wsf, .wsh, .ps1
②白名单
在文件上传防护中,使用白名单过滤是一个有效的方法,通过只允许特定的、安全的文件类型和扩展名来降低风险。以下是如何实现白名单过滤的详细步骤及示例代码:
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
?>
$ext_arr = array('jpg','png','gif');
设置了允许上传文件后缀的白名单列表;当文件后缀名在白名单中才能够成功上传文件。
绕过方式:
00截断:当文件名中包含NULL字符时,某些编程语言和服务器会在处理字符串时将NULL字符后的内容截断。例如,在PHP中,example.php\0.jpg
文件名在某些情况下会被截断为example.php
,从而绕过扩展名检查。
其他绕过手法总结
1.大小写绕过:利用服务器文件系统或代码对文件扩展名大小写不敏感的特性,绕过安全检测和过滤。
一些文件系统不区分文件名的大小写,这意味着 example.php
和 example.PHP
被视为同一个文件。在这种情况下,攻击者可以使用大小写变种绕过文件扩展名的检测。
Windows 文件系统(NTFS、FAT32):默认情况下不区分大小写。 macOS 文件系统(HFS+):默认情况下不区分大小写。 Linux 文件系统(ext3、ext4、XFS):默认情况下区分大小写。
但是在上述代码中包含将获取的扩展名进行统一转换的操作:
$file_ext = strtolower($file_ext); //转换为小写
所以此时大小写绕过放在上一个环境中无法生效。
2.双重扩展名
通过使用双重扩展名,服务器可能只检查第一个扩展名,从而绕过检测。
示例:
-
example.php.aa
-
example.php.bb
此时中间件在进行解析时会从后往前进行识别,将自身能够辨别的后缀名作为当前文件的类型。
3.文件内容混淆
原理:在文件内容中添加无害字符或注释,使其通过内容检查。
示例:
在PHP文件中添加JPEG文件头。
适用条件:服务器检查文件内容,但允许无害字符。
4.特殊字符绕过
原理:利用特殊字符或编码方式绕过检查。
示例:example.php%00.jpg
(%00 是 NULL 字符)
适用条件:服务器在处理字符串时忽略特殊字符或编码。
5.编码绕过
原理:通过URL编码或Unicode编码绕过检查。
示例:example%2Ephp
(%2E 代表 .)
适用条件:服务器在处理编码时不进行解码或不正确解码。
6.::$DATA
原理:利用NTFS文件系统的ADS特性,在文件名中加入::$DATA
。
示例:example.php::$DATA
适用条件:服务器运行在Windows且使用NTFS文件系统。
防御措施
1.严格的白名单机制:只允许特定的文件扩展名和MIME类型。 2.统一转换为小写:在处理扩展名时,统一将其转换为小写。 3。验证文件内容:不仅检查文件扩展名,还要检查文件内容的实际类型(MIME类型和文件头)。 4.移除特殊字符:在处理文件名时,移除包括NULL字符和::等特殊字符。 5.重命名上传文件:上传文件后,重命名文件以移除任何潜在的恶意扩展名。 6.设置正确的文件权限:确保上传目录没有执行权限,防止文件被执行。
示例代码
以下是一个综合了多种防御措施的文件上传代码示例:
<?php
function sanitize_filename($filename) {
// 移除NULL字符和NTFS数据流标识符以及其他特殊字符
$filename = str_replace(["\0", '::$DATA'], '', $filename);
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);
return $filename;
}
$allowed_ext = array('jpg', 'jpeg', 'png', 'pdf'); //后缀名白名单
$allowed_mime_types = array('image/jpeg', 'image/png', 'application/pdf'); //mime白名单
$file_name = sanitize_filename($_FILES['upload_file']['name']);
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$file_mime_type = finfo_file($finfo, $_FILES['upload_file']['tmp_name']);
finfo_close($finfo);
if (in_array($file_ext, $allowed_ext) && in_array($file_mime_type, $allowed_mime_types)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$destination = 'uploads/' . uniqid() . '.' . $file_ext;
if (move_uploaded_file($temp_file, $destination)) {
echo 'File uploaded successfully.';
} else {
echo 'Error moving uploaded file.';
}
} else {
echo 'Invalid file type.';
}
?>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="upload_file">
<button type="submit">Upload</button>
</form>
安全性分析
-
文件名清理:
sanitize_filename
函数移除NULL字符和NTFS数据流标识符,并只保留字母、数字、点、下划线和横杠。这有效地防止了常见的文件名绕过技术。 -
文件扩展名和MIME类型检查:
①检查文件扩展名是否在允许的列表中。
②使用
finfo_file
函数获取文件的MIME类型,并检查它是否在允许的列表中。这双重检查确保文件类型是安全的。 -
唯一文件名:使用
uniqid
函数生成唯一文件名,避免文件覆盖问题。 -
错误处理:如果文件移动失败,输出错误信息;这虽然不能完全防止攻击,但可以帮助调试和改进代码。