Wordpress File-manager 任意文件上传漏洞分析

紧急!WordPress文件管理器(file manager)插件爆严重0day漏洞

本文首发于安全客 Wordpress File-manager 任意文件上传漏洞分析

0x01 漏洞分析

分析环境:

  • wordpress 5.5.1
  • file manager 6.0
  • win10 phpstudy php 7.0.9

漏洞点位于file manager的connector.minimal.php文件,具体路径在wordpress\wp-content\plugins\wp-file-manager\lib\php\connector.minimal.php

image-20200908121359135

首先实例化一个elFinderConnector对象,然后调用它的run()方法,跟进run();

wp-content/plugins/wp-file-manager/lib/php/elFinderConnector.class.php

image-20200908133111974

如果HTTP请求的方法是POST会把POSTGET请求的保存到$src,然后判断POST传的参数。如果不传入targets,就不会进入前几个判断,之后会把POST传的cmd变量赋给$cmd,然后调用commandExists()检测传入的$cmd是否存在。

image-20200908125214608

然后利用commandArgsList()函数获取$cmd对应的命令参数列表,漏洞利用需要上传文件,这里只关注$cmdupload的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function commandArgsList($cmd)
{
if ($this->commandExists($cmd)) {
$list = $this->commands[$cmd];
$list['reqid'] = false;
} else {
$list = array();
}
return $list;
}
/*`upload`对应的数组如下:
'upload' => array(
'target' => true, 'FILES' => true, 'mimes' => false, 'html' => false, 'upload' => false,
'name' => false, 'upload_path' => false, 'chunk' => false, 'cid' => false, 'node' => false,
'renames' => false, 'hashes' => false, 'suffix' => false, 'mtime' => false, 'overwrite' => false,
'contentSaveId' => false)
*/

循环遍历,将POST传入的参数保存到$args数组中,然后调用input_filter()函数对$args进行简单的过滤,

image-20200908130150392

替换掉%00,并且做stripslashes()处理。然后将通过表单上传的文件$_FILES存到$args['FILE']中。然后调用exec()函数,跟进

wp-content/plugins/wp-file-manager/lib/php/elFinder.class.php

image-20200908132919783

前面会进行一些判断,最后进入到$this->$cmd($args)调用upload()函数,跟进

image-20200908151636282

首先将POST传入的target赋给$target变量,然后调用volume()函数,

image-20200908152053795

可以看到$this->volume数组含有两项,一项是l1_,一项是t1_volume()函数定义如果传入的$hashl1_t1_开头,返回$this->volume数组对应的值,否则返回false。在upload函数中会检测$volume,如果其为false,程序会报错结束,所以POST传入的target必须以它们两个为前缀。继续分析upload()函数。依次取出$args数组中的值赋给相应的变量,这里要求$args['FILES']['upload']也就是$_FILES['upload']为数组,才能将其赋给$files变量,这就需要上传文件时上传一个文件数组。接下来其他的如html、upload_path、chunk、cid、mtime等参数可以不传。接下来遍历$files['name']也就是$_FILES['upload']['name'],如果文件上传成功,将$_FILES['upload']['name']赋给$tmpname,然后调用fopen()打开上传的临时文件,将指针保存在$fp。在不传如upload_path$thash等于$target,所以$_target$target为我们POST传入的target变量。之后调用了$volume->upload()函数,第一个参数为之前打开文件的指针,第二个参数为POST传入的target变量,第三个参数为上传的文件名,第四个参数为空的数组。跟进elFinderVolumeDriverupload()

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
74
75
76
77
78
79
80
81
82
83
84
85
86
public function upload($fp, $dst, $name, $tmpname, $hashes = array())
{
if ($this->commandDisabled('upload')) {
return $this->setError(elFinder::ERROR_PERM_DENIED);
}

if (($dir = $this->dir($dst)) == false) {
return $this->setError(elFinder::ERROR_TRGDIR_NOT_FOUND, '#' . $dst);
}

if (empty($dir['write'])) {
return $this->setError(elFinder::ERROR_PERM_DENIED);
}

if (!$this->nameAccepted($name, false)) {
return $this->setError(elFinder::ERROR_INVALID_NAME);
}

$mimeByName = '';
if ($this->mimeDetect === 'internal') {
$mime = $this->mimetype($tmpname, $name);
} else {
$mime = $this->mimetype($tmpname, $name);
$mimeByName = $this->mimetype($name, true);
if ($mime === 'unknown') {
$mime = $mimeByName;
}
}

if (!$this->allowPutMime($mime) || ($mimeByName && !$this->allowPutMime($mimeByName))) {
return $this->setError(elFinder::ERROR_UPLOAD_FILE_MIME, '(' . $mime . ')');
}

$tmpsize = (int)sprintf('%u', filesize($tmpname));
if ($this->uploadMaxSize > 0 && $tmpsize > $this->uploadMaxSize) {
return $this->setError(elFinder::ERROR_UPLOAD_FILE_SIZE);
}

$dstpath = $this->decode($dst);
if (isset($hashes[$name])) {
$test = $this->decode($hashes[$name]);
$file = $this->stat($test);
} else {
$test = $this->joinPathCE($dstpath, $name);
$file = $this->isNameExists($test);
}

$this->clearcache();

if ($file && $file['name'] === $name) { // file exists and check filename for item ID based filesystem
if ($this->uploadOverwrite) {
if (!$file['write']) {
return $this->setError(elFinder::ERROR_PERM_DENIED);
} elseif ($file['mime'] == 'directory') {
return $this->setError(elFinder::ERROR_NOT_REPLACE, $name);
}
$this->remove($test);
} else {
$name = $this->uniqueName($dstpath, $name, '-', false);
}
}

$stat = array(
'mime' => $mime,
'width' => 0,
'height' => 0,
'size' => $tmpsize);

// $w = $h = 0;
if (strpos($mime, 'image') === 0 && ($s = getimagesize($tmpname))) {
$stat['width'] = $s[0];
$stat['height'] = $s[1];
}
// $this->clearcache();
if (($path = $this->saveCE($fp, $dstpath, $name, $stat)) == false) {
return false;
}

$stat = $this->stat($path);
// Try get URL
if (empty($stat['url']) && ($url = $this->getContentUrl($stat['hash']))) {
$stat['url'] = $url;
}

return $stat;
}

首先进入commandDisabled()函数,返回false。

image-20200908150731360

然后进入dir()函数,参数为$dstPOST传入的target值。

image-20200908153636101

调用了file函数,

image-20200908153733922

跟进decode()函数

image-20200908153822619

image-20200908154015274

decode()函数首先判断是否以$this->id开头,然后截取出l1_后面的内容,之后进行base64解密,uncrypt函数如上,未作操作。然后更换分隔符,之后调用abspathCE()函数,从注释中可以看出,abspathCE()函数会先判断$path是否等于分隔符\,如果等于,返回$this->root,否则返回$this->root拼接$path。看下对应的abspathCE()函数。

image-20200908160039652

image-20200908155954458

image-20200908155946798

ps:POST传入target前缀不同的区别

  • 前缀为l1_时,$this->rootC:\Users\admin\phpstudy_pro\WWW\wordpress\wp-content\plugins\wp-file-manager\lib\files

image-20200908161235911

  • 前缀为t1_时,$this->disabled[]包含upload,程序会报错结束,$this->rootC:\Users\admin\phpstudy_pro\WWW\wordpress\wp-content\plugins\wp-file-manager\lib\files\.trash

image-20200908160836714

继续分析程序流程,decode()函数会返回C:\Users\admin\phpstudy_pro\WWW\wordpress\wp-content\plugins\wp-file-manager\lib\files,然后调用stat()函数。

image-20200908153733922

image-20200908162459331

返回的$ret

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
74
75
76
77
78
79
80
81
82
83
84
85
Array
(
[isowner] =>
[ts] => 1589423646
[mime] => directory
[read] => 1
[write] => 1
[size] => 0
[hash] => l1_Lw
[name] => files
[rootRev] =>
[options] => Array
(
[path] =>
[url] => /wordpress/wp-content/plugins/wp-file-manager/lib/php/../files/
[tmbUrl] => /wordpress/wp-content/plugins/wp-file-manager/lib/php/../files/.tmb/
[disabled] => Array
(
[0] => chmod
)

[separator] => \
[copyOverwrite] => 1
[uploadOverwrite] => 1
[uploadMaxSize] => 9223372036854775807
[uploadMaxConn] => 3
[uploadMime] => Array
(
[firstOrder] => deny
[allow] => Array
(
[0] => all
)

[deny] => Array
(
[0] => all
)

)

[dispInlineRegex] => ^(?:(?:video|audio)|image/(?!.+\+xml)|application/(?:ogg|x-mpegURL|dash\+xml)|(?:text/plain|application/pdf)$)
[jpgQuality] => 100
[archivers] => Array
(
[create] => Array
(
[0] => application/x-tar
[1] => application/zip
)

[extract] => Array
(
[0] => application/x-tar
[1] => application/zip
)

[createext] => Array
(
[application/x-tar] => tar
[application/zip] => zip
)

)

[uiCmdMap] => Array
(
)

[syncChkAsTs] => 1
[syncMinMs] => 10000
[i18nFolderName] => 0
[tmbCrop] => 1
[tmbReqCustomData] =>
[substituteImg] => 1
[onetimeUrl] => 1
[trashHash] => t1_Lw
[csscls] => elfinder-navbar-root-local
)

[volumeid] => l1_
[locked] => 1
[isroot] => 1
[phash] =>
)

返回dir()函数,然后在返回到upload()函数,将返回值赋给upload()函数中的$dir变量,

image-20200908172728778

然后进行mime的判断,程序识别上传的php脚本的mimetext/x-php,跟进allowPutMime()函数,

image-20200908175400724

从程序自带的注释中可以看出如果uploadOrder数组为array('deny','allow'),则默认时允许上传的mime。然后获取文件的大小,若文件大小不合法报错结束程序,之后decode()处理$dst(POST传入的target)赋给$dstpath,因为$hash为空数组,所以会调用joinPathCE()$dstpath$name(上传文件的文件名)拼接,然后检查文件是否存在。

image-20200908180818104

最后调用$this->saveCE()

image-20200908181044918

跟进_save()

image-20200908181932277

本地时利用Windows系统分析,$pathC:\Users\admin\phpstudy_pro\WWW\wordpress\wp-content\plugins\wp-file-manager\lib\files\shell.php;$uriC:\Windows\phpxxxx.tmp,最后会调用copy()将上传的文件复制到\wordpress\wp-content\plugins\wp-file-manager\lib\files\shell.php,即完成了任意文件上传。

image-20200908184402757

0x02 漏洞利用

利用burp发包

image-20200908183438085

访问http://192.168.43.44/wordpress/wp-content/plugins/wp-file-manager/lib/files/shell.php

image-20200908183522240

0x03 漏洞修复

影响范围

file manager 6.0至6.8

官方修复删除了connector.minimal.php和connector.minimal.php-dist文件。

image-20200908183148666

增加了.htaccess。

image-20200908183049056

参考:

紧急!WordPress文件管理器插件爆严重0day漏洞