文件上传漏洞可以说是日常渗透测试用得最多的一个漏洞,因为用它获得服务器权限最快最直接。但是想真正把这个漏洞利用好却不那么容易,其中有很多技巧,也有很多需要掌握的知识。俗话说,知己知彼方能百战不殆,因此想要研究怎么防护漏洞,就要了解怎么去利用。

特点

1、利用简单

2、危害大

产生原因

缺少必要的校验

代码审计

基础

关于 PHP 中 $_FILES 数组的使用方法

$_FILES[‘file’][‘name’] 客户端文件名称

$_FILES[‘file’][‘type’] 文件的 MIME 类型

$_FILES[‘file’][‘size’] 文件大小单位字节

$_FILES[‘file’][‘tmp_name’] 文件被上传后再服务器端临时文件名,可以在 php.ini 中指定

需要注意的是在文件上传结束后,默认的被储存在临时文件夹中,这时必须把他从临时目录中删除或移动到其他地方,否则,脚本运行完毕后,自动删除临时文件,可以使用 copy 或者 *move_uploaded_file 两个函数

程序员对某些常用函数的错误认识

这些函数有: empty()、isset()、strpos()、rename() 等,如下面的代码:

#!php
if($operateId == 1){
    $date = date("Ymd");
    $dest = $CONFIG->basePath."data/files/".$date."/";
    $COMMON->createDir($dest);
    //if (!is_dir($dest))   mkdir($dest, 0777);
    $nameExt = strtolower($COMMON->getFileExtName($_FILES['Filedata']['name']));
    $allowedType = array('jpg', 'gif', 'bmp', 'png', 'jpeg');
    if(!in_array($nameExt, $allowedType)){
        $msg = 0;
    }
    if(empty($msg)){
        $filename = getmicrotime().'.'.$nameExt;
        $file_url = urlencode($CONFIG->baseUrl.'data/files/'.$date."/".$filename);
        $filename = $dest.$filename;
        if(empty($_FILES['Filedata']['error'])){
            move_uploaded_file($_FILES['Filedata']['tmp_name'],$filename);
        }
        if (file_exists($filename)){
            //$msg = 1;
            $msg = $file_url;
            @chmod($filename, 0444);
        }else{
            $msg = 0;
        }
    }
    $outMsg = "fileUrl=".$msg;
    $_SESSION["eoutmsg"] = $outMsg;
    exit;
}

我们来看上面的这段代码,要想文件成功的上传, if(empty($msg)) 必须为 True 才能进入 if 的分支,接下来我们来看 empty 函数何时返回 True,看看 PHP Manual 怎么说,如图:

文件上传JavaScript 文件上传漏洞防御方法_ico

很明显,""、0、"0"、NULL、FALSE、array()、var $var; 以及没有任何属性的对象都将被认为是空的,如果 var 为空,则返回 True。 非常好,接下来我们往回看,有这样的几行代码

#!php
$allowedType = array('jpg', 'gif', 'bmp', 'png', 'jpeg');

if(!in_array($nameExt, $allowedType)){
    $msg = 0;
}

看见没有,即使我们上传类似 shell.php 的文件,虽然程序的安全检查把 $msg 赋值为 0,经 empty($msg) 后,仍然返回 True,于是我们利用这个逻辑缺陷即可成功的上传 shell.php。

程序员对某些常用函数的错误使用

这些函数有 iconv()、copy() 等,如下面的这段代码(摘自 SiteStar)

#!php
public function img_create(){
     $file_info =& ParamHolder::get('img_name', array(), PS_FILES);
     if($file_info['error'] > 0){
         Notice::set('mod_marquee/msg', __('Invalid post file data!'));
         Content::redirect(Html::uriquery('mod_tool', 'upload_img'));
     }
     if(!preg_match('/\.('.PIC_ALLOW_EXT.')$/i', $file_info["name"])){
         Notice::set('mod_marquee/msg', __('File type error!'));
         Content::redirect(Html::uriquery('mod_marquee', 'upload_img'));
     }
     if(file_exists(ROOT.'/upload/image/'.$file_info["name"])){
         $file_info["name"] = Toolkit::randomStr(8).strrchr($file_info["name"],".");
     }
     if(!$this->_savelinkimg($file_info)){
         Notice::set('mod_marquee/msg', __('Link image upload failed!'));
         Content::redirect(Html::uriquery('mod_marquee', 'upload_img'));
      }
      //...
 }
private function _savelinkimg($struct_file){
    $struct_file['name'] = iconv("UTF-8", "gb2312", $struct_file['name']);
    move_uploaded_file($struct_file['tmp_name'], ROOT.'/upload/image/'.$struct_file['name']);
    return ParamParser::fire_virus(ROOT.'/upload/image/'.$struct_file['name']);
}

我们再来看看这段代码, img_create() 函数的逻辑非常严密,安全检查做的很到位。然而问题出在了 _savelinkimg() 函数,即在保存文件的前面程序员错误的使用了 iconv() 函数,并且文件名经过了此函数,为什么是错用了呢?

因为啊 iconv 函数在转码过程中,可能存在字符串截断的问题:

iconv 转码的过程中, utf->gb2312 (其他部分编码之间转换同样存在这个问题)会导致字符串被截断,如:

$filename="shell.php(hex).jpg";

(hex 为 0x80-0x99),经过 iconv 转码后会变成 $filename="shell.php ";

所以,经过 iconv 后 $struct_file['name']) 为 shell.php,于是我们利用这个逻辑缺陷可以成功的上传 shell.php (前提是上传的文件名为 shell.php{%80-%99}.jpg)

历史经典漏洞再次爆发

条件竞争漏洞,这类历史经典漏洞在逐渐淡出人们视线的时候,再次爆发..

接着看下面这段代码(摘自某 VPN 系统)

#!php
<?
if($_POST['realfile']){
    copy($_POST['realfile'],$_POST['path']);
}
$file = mb_convert_encoding($_POST[file],"GBK","UTF-8");
header("Pragma:");
header("Cache-Control:");
header("Content-type:application/octet-stream");
header("Content-Length:".filesize($_POST[path]));
header("Content-Disposition:attachment;filename=\"$file\"");
readfile($_POST[path]);
if($_POST['realfile']){
    unlink($_POST["path"]);
}
?>

上述代码的逻辑表面上看起来是这样的(对于攻击者来说):

利用 copy 函数,将 realfile 生成 shell.php 然后删除掉 shell.php

这样初看起来没办法利用,但是仔细一想, 这段代码其实是存在逻辑问题的,所以我们可以利用这个逻辑缺陷达到 GetShell 的目的。

具体利用方法:

copy 成 temp.php --> 不断访问 temp.php --> temp.php 生成 shell.php --> 删除 temp.php --> 留下 shell.php

校验方式分类&总结

客户端 javascript 校验(一般只校验后缀名)

服务端校验

1、文件头 content-type 字段校验(image/gif)

2、文件内容头校验(GIF89a)

3、后缀名黑名单校验

4、后缀名白名单校验

5、自定义正则校验

6、WAF 设备校验(根据不同的 WAF 产品而定)

校验方式溯源

通常一个文件以 HTTP 协议进行上传时,将以 POST 请求发送至 Web 服务器,Web 服务器接收到请求并同意后,用户与 Web 服务器将建立连接,并传输数据。一般文件上传过程中将会经过如下几个检测步骤:

文件上传JavaScript 文件上传漏洞防御方法_上传_02

校验方式&绕过姿势

PUT 方法

WebDAV 是一种基于 HTTP 1.1 协议的通信协议.它扩展了 HTTP 1.1,在 GET、POST、HEAD 等几个 HTTP 标准方法以外添加了一些新的方法。使应用程序可直接对 Web Server 直接读写,并支持写文件锁定 (Locking) 及解锁 (Unlock),还可以支持文件的版本控制。当 WebDAV 开启 PUT,MOVE,COPY,DELETE 方法时,攻击者就可以向服务器上传危险脚本文件。

此时可以使用 OPTIONS 探测服务器支持的 http 方法,如果支持 PUT,就进行上传脚本文件,在通过 MOVE 或 COPY 方法改名。

当开启 DELETE 时还可以删除文件。

参考:

http://wiki.wooyun.org/server:httpput

客户端校验

JavaScript 校验

验证代码如下:

<?php
//文件上传漏洞演示脚本之js验证
$uploaddir = 'uploads/';
if (isset($_POST['submit'])) {
    if (file_exists($uploaddir)) {
        if (move_uploaded_file($_FILES['upfile']['tmp_name'], $uploaddir . '/' . $_FILES['upfile']['name'])) {
            echo '文件上传成功,保存于:' . $uploaddir . $_FILES['upfile']['name'] . "\n";
        }
    } else {
        exit($uploaddir . '文件夹不存在,请手工创建!');
    }
    //print_r($_FILES);
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=gbk"/>
    <meta http-equiv="content-language" content="zh-CN"/>
    <title>文件上传漏洞演示脚本--JS验证实例</title>
    <script type="text/javascript">
       function checkFile() {
            var file = document.getElementsByName('upfile')[0].value;
            if (file == null || file == "") {
                alert("你还没有选择任何文件,不能上传!");
                return false;
            }
            //定义允许上传的文件类型
            var allow_ext = ".jpg|.jpeg|.png|.gif|.bmp|";
            //提取上传文件的类型
            var ext_name = file.substring(file.lastIndexOf("."));
            //alert(ext_name);
            //alert(ext_name + "|");
            //判断上传文件类型是否允许上传
            if (allow_ext.indexOf(ext_name + "|") == -1) {
                var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" +     ext_name;
                alert(errMsg);
                return false;
            }
        }
    </script>
<body>
<h3>文件上传漏洞演示脚本--JS验证实例</h3>
<form action="" method="post" enctype="multipart/form-data" name="upload" onsubmit="return     checkFile()">
    <input type="hidden" name="MAX_FILE_SIZE" value="204800"/>
    请选择要上传的文件:<input type="file" name="upfile"/>
    <input type="submit" name="submit" value="上传"/>
</form