朝花夕拾|勿忘初心 朝花夕拾|勿忘初心

Drupal 8 配置文件下载漏洞分析

in 漏洞分析 read (541) 903汉字 站长Lucifaer 文章转载请注明来源!

漏洞总体来说并不是很严重,分析起来也比较简单,想联系一下动态调试的亲们不要错过这个洞。

0x00 漏洞概述

1. 漏洞简介

Drupal是一个自由开源的内容管理系统,在其8.x<8.1.10的版本中存在一个漏洞攻击者可以在未授权的情况下下载管理员之前导出的配置文件压缩包config.tar.gz

2. 漏洞影响版本

Drupal 8.x<8.1.10

0x01 漏洞复现

可以在Drupal官方网站的代码归档中下载有漏洞版本的源码。

这边使用的是Drupal 8.0.5

0x02 漏洞分析

首先进入后台,将配置文件导出,

默认导出路径是/tmp/

分析路由项

core/modules/system/system.routing.yml找到了Drupal对于访问管理页面的路由项:

可以看到在这里限制了permission,指定了需要adminnistrator的权限。

顺藤摸瓜,往下看,看到了三个未授权可访问页面:

其中,我们专注看一下这一项:

根据其路由可以看到这是一个处理文件下载时的模块,并且这个模块是可以非授权访问的,可能存在文件下载漏洞。跟进它的controller,根据路由知道位置在core/modules/system/src/FileDownloadController.php

  public function download(Request $request, $scheme = 'private') {
    $target = $request->query->get('file');
    // Merge remaining path arguments into relative file path.
    $uri = $scheme . '://' . $target;

    if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
      // Let other modules provide headers and controls access to the file.
      $headers = $this->moduleHandler()->invokeAll('file_download', array($uri));

      foreach ($headers as $result) {
        if ($result == -1) {
          throw new AccessDeniedHttpException();
        }
      }

      if (count($headers)) {
        // \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond()
        // sets response as not cacheable if the Cache-Control header is not
        // already modified. We pass in FALSE for non-private schemes for the
        // $public parameter to make sure we don't change the headers.
        return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');
      }

      throw new AccessDeniedHttpException();
    }

    throw new NotFoundHttpException();
  }

}

动态调试

根据上面的分析,可以看到download函数首先接收了file参数,之后与$scheme进行拼接。为了测试其中参数的变化,我们首先访问http://127.0.0.1/drupal/system/temporary/?file=config.tar.gz,然后在$target这里下断点动态调试:

可以看到$target=config.tar.gz$scheme=temporary;也就是$uri=temporary://config.tar.gz

接着往下走,看到跳转到/core/includes/file.inc中的file_stream_wrapper_valid_scheme方法中:

function file_stream_wrapper_valid_scheme($scheme) {
  return \Drupal::service('file_system')->validScheme($scheme);
}

在这里检查URI流是否存在,也就是检查$theme是否存在。

接下来我们继续步入调试,可以看到其中调用了Drupal::service方法,在/core/lib/Drupal.php中:

public static function service($id) {
    return static::getContainer()->get($id);
  }

可以看到我们现在传入的$id=file_system,通过该方法,来从controller中寻找服务,同时返回该服务。

继续步入,发现调用到了/core/lib/Drupal/Component/DependencyInjection/Container.php这个controller,并调用其中的get方法,在该方法中,将我们传入的参数进行存在性判断,并将需要定义的服务、调用的模块进行序列化操作,经过验证后,将需要的服务创建出来:

接下来就是创建服务的过程,我们可以直接Step over,也就是F8过去,到了/core/lib/Drupal/Core/StreamWrapper/LocalStream.php中的url_stat方法时,稍微停一下。我们注意到这个文件是处理内部数据流的文件:

/**
   * Support for stat().
   *
   * @param string $uri
   *   A string containing the URI to get information about.
   * @param int $flags
   *   A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
   *
   * @return array
   *   An array with file status, or FALSE in case of an error - see fstat()
   *   for a description of this array.
   *
   * @see http://php.net/manual/streamwrapper.url-stat.php
   */
  public function url_stat($uri, $flags) {
    $this->uri = $uri;
    $path = $this->getLocalPath();
    // Suppress warnings if requested or if the file or directory does not
    // exist. This is consistent with PHP's plain filesystem stream wrapper.
    if ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) {
      return @stat($path);
    }
    else {
      return stat($path);
    }
  }

留心一下注释,可以看到url_stat这个方法是用来获取URI字段的信息的,我们单步调试一下:

这里可以看到通过getLocalPath方法,我们获得到了文件的本地真实路径。

接下来回到我们原来的download方法中:

if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
      // Let other modules provide headers and controls access to the file.
      $headers = $this->moduleHandler()->invokeAll('file_download', array($uri));

      foreach ($headers as $result) {
        if ($result == -1) {
          throw new AccessDeniedHttpException();
        }
      }

继续跟进invokeAll方法中,文件位置在/core/lib/Drupal/Core/Extension/ModuleHandler.php中:

public function invokeAll($hook, array $args = array()) {
    $return = array();
    $implementations = $this->getImplementations($hook);
    foreach ($implementations as $module) {
      $function = $module . '_' . $hook;
      $result = call_user_func_array($function, $args);
      if (isset($result) && is_array($result)) {
        $return = NestedArray::mergeDeep($return, $result);
      }
      elseif (isset($result)) {
        $return[] = $result;
      }
    }

    return $return;
  }

可以看到最后call_usr_func_array中可选择调用的函数有三个:

  • config_file_download
  • file_file_download
  • image_file_download

首先调用的是config_file_download,跟进看一下:

function config_file_download($uri) {
  $scheme = file_uri_scheme($uri);
  $target = file_uri_target($uri);
  if ($scheme == 'temporary' && $target == 'config.tar.gz') {
    $request = \Drupal::request();
    $date = DateTime::createFromFormat('U', $request->server->get('REQUEST_TIME'));
    $date_string = $date->format('Y-m-d-H-i');
    $hostname = str_replace('.', '-', $request->getHttpHost());
    $filename = 'config' . '-' . $hostname . '-' . $date_string. '.tar.gz';
    $disposition = 'attachment; filename="' . $filename . '"';
    return array(
      'Content-disposition' => $disposition,
    );
  }
}

明显的看出,当$scheme=='temporary'并且$target=='config.tar.gz'时,就可以完成下载。

最后invokeAll的返回值:

另外一点想法

这点在我看小伙伴文章的时候,发现同样也想到了。当时我是这样想的,既然现在我们有一个可以管理员配置文件下载的突破口,那么可不可以利用00截断的方法造成任意文件下载的漏洞,但是在动态调的时候,在url_stat中的getLocalPath方法,使用了realpath函数限制了目录跳转:

protected function getLocalPath($uri = NULL) {
    if (!isset($uri)) {
      $uri = $this->uri;
    }
    $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri);

    // In PHPUnit tests, the base path for local streams may be a virtual
    // filesystem stream wrapper URI, in which case this local stream acts like
    // a proxy. realpath() is not supported by vfsStream, because a virtual
    // file system does not have a real filepath.
    if (strpos($path, 'vfs://') === 0) {
      return $path;
    }

    $realpath = realpath($path);
    if (!$realpath) {
      // This file does not yet exist.
      $realpath = realpath(dirname($path)) . '/' . drupal_basename($path);
    }
    $directory = realpath($this->getDirectoryPath());
    if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) {
      return FALSE;
    }
    return $realpath;
  }

所以说,这个漏洞只能在管理员导出备份的情况下下载/tmp/config.tar.gz

0x03 diff 比较

新版本在/core/modules/config/config.module中对$scheme=='temporary'&&$target=='config.tar.gz'增加了权限检测。

0x04 修补方案

升级Drupal版本吧,还有就是管理员对于配置文件的导出要注意及时转移,别太天真....

php漏洞分析
最后由Lucifaer修改于2017-06-01 01:31

此处评论已关闭

博客已萌萌哒运行
© 2018 由 Typecho 强力驱动.Theme by Yodu
PREVIOUS NEXT
雷姆
拉姆