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

阅读量472774

|

发布时间 : 2020-09-10 15:30:58

 

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

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

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

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

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进行简单的过滤,

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

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

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

可以看到$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()

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。

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

调用了file函数,

跟进decode()函数

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

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

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

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

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

stat()函数返回的$ret

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变量,

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

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

最后调用$this->saveCE()

跟进_save()

本地是利用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,即完成了任意文件上传。

 

0x02 漏洞利用

利用burp发包

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

 

0x03 漏洞修复

影响范围

file manager 6.0至6.8

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

增加了.htaccess。

参考

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

本文由Mount4in原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/216990

安全客 - 有思想的安全新媒体

分享到:微信
+19赞
收藏
Mount4in
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66