浅析文件上传漏洞
2020-03-18 10:53:53 Author: xz.aliyun.com(查看原文) 阅读量:249 收藏

文件上传

"文件上传"功能已经成为现在Web应用的一种常见需求,它不但有助于提供业务效率(例如:企业内部文件共享),更有助于优化用户的体验(例如:上传视频、图片、头像等各种其他类型的文件)。"文件上传"功能一方面带来了良好的体验,另一方面也带来的“安全问题”。目前文件上传漏洞已经成为web安全中经常利用到的一种漏洞形式,对于缺少安全防护的web应用,攻击者可以利用提供的文件上传功能将恶意代码植入到服务器中,之后再通过url去访问以执行代码达到攻击的目的。

漏洞成因

造成文件上传漏洞的原因有:

  • 开源编辑器的上传漏洞
  • 服务器配置不当
  • 本地文件上传限制被绕过
  • 过滤不严或被绕过
  • 文件解析漏洞导致文件执行
  • 文件路径截断

利用条件

  • 恶意文件可以成功上传
  • 恶意文件上传后的路径
  • 恶意文件可被访问或执行

文件检测

首先我们可以先来了解一下文件上传的过程,一个文件上传需要经过哪些检测流程或者说可以有哪些检测流程对文件进行检测~
通常一个文件以HTTP协议进行上传时,将以POST请求发送至web服务器,web服务器接收到请求后并同意后,用户与web 服务器将建立连接,并传输data:

而一般一个文件上传过程中的检测如下图红色标记部分:

A 客户端 javascript 检测 (通常为检测文件扩展名)
B 服务端 MIME 类型检测 (检测 Content-Type 内容)
C 服务端目录路径检测 (检测跟 path 参数相关的内容)
D 服务端文件扩展名检测 (检测跟文件 extension 相关的内容)
E 服务端文件内容检测 (检测内容是否合法或含有恶意代码)

检测绕过

客户端JavaScript绕过

这类检测通常在上传页面里含有专门检测文件上传的javascript代码,最常见的就是检测扩展名是否合法:

对于上面的客户端检测方式可以通过禁用前端JS或者使用burpsuite修改请求数据包的方法来绕过,下面以upload-labs中的Pass-01为例做演示,主要代码如下所示:

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $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 = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}
?>

<div id="upload_panel">
    <ol>
        <li>
            <h3>任务</h3>
            <p>上传一个<code>webshell</code>到服务器。</p>
        </li>
        <li>
            <h3>上传区</h3>
            <form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
                <p>请选择要上传的图片:<p>
                <input class="input_file" type="file" name="upload_file"/>
                <input class="button" type="submit" name="submit" value="上传"/>
            </form>
            <div id="msg">
                <?php 
                    if($msg != null){
                        echo "提示:".$msg;
                    }
                ?>
            </div>
            <div id="img">
                <?php
                    if($is_upload){
                        echo '<img src="'.$img_path.'" width="250px" />';
                    }
                ?>
            </div>
        </li>
        <?php 
            if($_GET['action'] == "show_code"){
                include 'show_code.php';
            }
        ?>
    </ol>
</div>

<?php
include '../footer.php'
?>


<script type="text/javascript">
    function checkFile() {
        var file = document.getElementsByName('upload_file')[0].value;
        if (file == null || file == "") {
            alert("请选择要上传的文件!");
            return false;
        }
        //定义允许上传的文件类型
        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;
        }
    }
</script>

从上述代码可以看到,这里首先构建了一个上传表单,在提交时会自动调用前端JS中的checkFile()函数来对文件的类型进行检查,如果隶属于允许上传的文件类型则会通过校验,否则以弹框的方式提示文件格式不允许,这是一种典型的客户端校验。
在常规的渗透测试过程中,我们根本无法获取到目标网站对于上传文件校验的源代码设计,那么如何判断文件是客户端校验还是服务器端校验呢?我们可以通过burpsuite来进行简易判断,具体流程如下:
首先,在本地创建一个shell.php文件,之后直接上传该文件,同时这是burpsuite作为代理,如果burpsuite未捕获到通信数据包且浏览器端已完对文件的校验,那么这种方式就属于客户端校验,例如:

禁用JS绕过客户端校验

1、在Firefox地址栏里输入“about:config”
2、在搜索栏输入“javascript.enabled”查找到首选项。
3、点击鼠标右键选择“切换”,把“javascript.enabled”键值改为“false”

之后再次上传shell.php文件,发现成功上传,且直接忽视前端js的校验:

服务器端成功上传shell.php文件:

burpsuite改包绕过客户端校验

首先,修改shell.php为shell.jpg,这里修改的目的是绕过前端的校验,之后上传该shell.jpg文件,同时使用burpsuite抓包:

之后修改shell.jpg为shell.php,释放数据包:

之后成功上传shell.php

shell.php已被成功上传到服务器端:

使用菜刀成功连接shell:

服务器端MIME类型检测绕过

MIME类型检测属于白名单检测的一种,在服务器端完成,它会对上传文件请求包中Content-Type的内容进行校验,判断是否属于白名单,如果不属于则不允许上传。

下面以upload-labs的Pass-02为例做绕过演示,具体代码如下:

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

$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.'文件夹不存在,请手工创建!';
    }
}
?>

<div id="upload_panel">
    <ol>
        <li>
            <h3>任务</h3>
            <p>上传一个<code>webshell</code>到服务器。</p>
        </li>
        <li>
            <h3>上传区</h3>
            <form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
                <p>请选择要上传的图片:<p>
                <input class="input_file" type="file" name="upload_file"/>
                <input class="button" type="submit" name="submit" value="上传"/>
            </form>
            <div id="msg">
                <?php 
                    if($msg != null){
                        echo "提示:".$msg;
                    }
                ?>
            </div>
            <div id="img">
                <?php
                    if($is_upload){
                        echo '<img src="'.$img_path.'" width="250px" />';
                    }
                ?>
            </div>
        </li>
        <?php 
            if($_GET['action'] == "show_code"){
                include 'show_code.php';
            }
        ?>
    </ol>
</div>

<?php
include '../footer.php';
?>

从以上代码中可以看到,此处对文件的Type进行了检查,只允许:image/jpeg、image/png、image/gif三种格式,而且直接为服务器端校验,对于服务端的这种校验方式,我们可以通过burpsuite抓包来更改content-Type为其允许的类型即可!
这里模拟一下黑盒测试,首先创建一个shell.php文件,之后上传该文件:

从上面可以看到,先上传文件,之后通信数据经过burpsuite,之后再回显"提示:文件类型不正确,请重新上传!",所以,此处检测为服务器端检测,而且提示的关键信息为"文件类型不正确",由此定位检测点为content-type!
之后上传shell.php,同时使用burpsuite抓包:

修改content-type为:image/jpeg

之后释放数据包,成功上传shell.php

服务器端成功上传shell.php:

服务器端目录路径检测绕过

目录路径检测,一般就检测路径是否合法:

针对上面的检测方法可以通过00截断的方式来绕过,下面以upload-labs的Pass-12为例:

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

$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类型文件!";
    }
}
?>

<div id="upload_panel">
    <ol>
        <li>
            <h3>任务</h3>
            <p>上传一个<code>webshell</code>到服务器。</p>
        </li>
        <li>
            <h3>上传区</h3>
            <form action="?save_path=../upload/" enctype="multipart/form-data" method="post">
                <p>请选择要上传的图片:<p>
                <input class="input_file" type="file" name="upload_file"/>
                <input class="button" type="submit" name="submit" value="上传"/>
            </form>
            <div id="msg">
                <?php 
                    if($msg != null){
                        echo "提示:".$msg;
                    }
                ?>
            </div>
            <div id="img">
                <?php
                    if($is_upload){
                        echo '<img src="'.$img_path.'" width="250px" />';
                    }
                ?>
            </div>
        </li>
        <?php 
            if($_GET['action'] == "show_code"){
                include 'show_code.php';
            }
        ?>
    </ol>
</div>

<?php
include '../footer.php';
?>

这里采用了白名单判断,但是$img_path直接拼接,所以可以先上传一个符合白名单检测的jpg文件,之后再burpsuite中使用%00截断保存路径即可绕过检测,最后保存一个php类型的木马文件。
首先,上传一个shell.jpg格式的文件,之后使用burpsuite抓包:

之后在save_path处使用%00截断构造保存的文件名:

使用菜刀连接

文件扩展名检测绕过

这种检测方式会对文件的后缀名进行检测,常见的有白名单和黑名单两种。

假定服务器端采用黑名单检测方式,这里借助upload-labs来演示通过后缀名大小写绕过检测(其余情景还很多,例如:.htaccess攻击、空格绕过、解析绕过、双写后缀绕过等,有兴趣的可以自我研习),以下代码取自upload-labs的Pass-06

<?php
include '../config.php';
include '../common.php';
include '../head.php';
include '../menu.php';

$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",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $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 = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}
?>

<div id="upload_panel">
    <ol>
        <li>
            <h3>任务</h3>
            <p>上传一个<code>webshell</code>到服务器。</p>
        </li>
        <li>
            <h3>上传区</h3>
            <form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
                <p>请选择要上传的图片:<p>
                <input class="input_file" type="file" name="upload_file"/>
                <input class="button" type="submit" name="submit" value="上传"/>
            </form>
            <div id="msg">
                <?php 
                    if($msg != null){
                        echo "提示:".$msg;
                    }
                ?>
            </div>
            <div id="img">
                <?php
                    if($is_upload){
                        echo '<img src="'.$img_path.'" width="250px" />';
                    }
                ?>
            </div>
        </li>
        <?php 
            if($_GET['action'] == "show_code"){
                include 'show_code.php';
            }
        ?>
    </ol>
</div>

<?php
include '../footer.php';
?>

从以上代码中可以看到,这里采用了黑名单检测方法,但是在使用文件后缀名与黑名单中的后缀名进行匹配时未先转小写,导致可以使用大小写后缀的方式来绕过。
直接上传shell.PHp文件:

从回显结果来看成功上传:

服务器端文件内容检测绕过

文件内容检测及对上传的文件的内容进行检查,常见检查方法有:幻数检测、exif_imagetype()检测、getimagesize()检测、二次渲染等。

这里以幻数检测绕过为例做一个简单的演示,以下代码来自upload-labs的Pass-14:

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

function getReailFileType($filename){
    $file = fopen($filename, "rb");
    $bin = fread($file, 2); //只读2字节
    fclose($file);
    $strInfo = @unpack("C2chars", $bin);    
    $typeCode = intval($strInfo['chars1'].$strInfo['chars2']);    
    $fileType = '';    
    switch($typeCode){      
        case 255216:            
            $fileType = 'jpg';
            break;
        case 13780:            
            $fileType = 'png';
            break;        
        case 7173:            
            $fileType = 'gif';
            break;
        default:            
            $fileType = 'unknown';
        }    
        return $fileType;
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_type = getReailFileType($temp_file);

    if($file_type == 'unknown'){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}
?>

<div id="upload_panel">
    <ol>
        <li>
            <h3>任务</h3>
            <p>上传<code>图片马</code>到服务器。</p>
            <p>注意:</p>
            <p>1.保证上传后的图片马中仍然包含完整的<code>一句话</code>或<code>webshell</code>代码。</p>
            <p>2.使用<a href="<?php echo INC_VUL_PATH;?>" target="_bank">文件包含漏洞</a>能运行图片马中的恶意代码。</p>
            <p>3.图片马要<code>.jpg</code>,<code>.png</code>,<code>.gif</code>三种后缀都上传成功才算过关!</p>
        </li>
        <li>
            <h3>上传区</h3>
            <form enctype="multipart/form-data" method="post">
                <p>请选择要上传的图片:<p>
                <input class="input_file" type="file" name="upload_file"/>
                <input class="button" type="submit" name="submit" value="上传"/>
            </form>
            <div id="msg">
                <?php 
                    if($msg != null){
                        echo "提示:".$msg;
                    }
                ?>
            </div>
            <div id="img">
                <?php
                    if($is_upload){
                        echo '<img src="'.$img_path.'" width="250px" />';
                    }
                ?>
            </div>
        </li>
        <?php 
            if($_GET['action'] == "show_code"){
                include 'show_code.php';
            }
        ?>
    </ol>
</div>

<?php
include '../footer.php';
?>

从代码层面来看应该是检测的文件的幻数,那么我们在文件内容的开头增加幻数就OK!
首先,上传shell.jpg,同时使用burpsuite抓包:

之后在文件内容处加入幻数:

从上面可以看到成功上传文件,该漏洞的利用还需要借助文件包含漏洞,例如:

防御策略

1、严格规范文件上传处理逻辑设计,不建议先存储后判断的方式,以免引起条件竞争类漏洞。
2、严格检查上传的文件后缀名、Content-Type、文件内容等。
3、定时修复服务器端的解析类漏洞,以及文件包含类漏洞避免漏洞组合。


文章来源: http://xz.aliyun.com/t/7365
如有侵权请联系:admin#unsafe.sh