Author: p0wd3r (知道創宇404安全實驗室)

Date: 2016-09-22

0x00 漏洞概述

1.漏洞簡介

Drupal ( https://www.drupal.org )是一個自由開源的內容管理系統,近期研究者發現在其8.x < 8.1.10的版本中發現了三個安全漏洞,其中一個漏洞攻擊者可以在未授權的情況下下載管理員之前導出的配置文件壓縮包config.tar.gz。Drupal官方在9月21日發布了升級公告( https://www.drupal.org/SA-CORE-2016-004 )。

2.漏洞影響

未授權狀態下下載管理員之前導出的配置文件

3.影響版本

8.x < 8.1.10

0x01 漏洞復現

1. 環境搭建

Dockerfile(來自Docker Hub)

# from https://www.drupal.org/requirements/php#drupalversions
FROM php:7.0-apache

RUN a2enmod rewrite

# install the PHP extensions we need
RUN apt-get update && apt-get install -y libpng12-dev libjpeg-dev libpq-dev \
    && rm -rf /var/lib/apt/lists/* \
    && docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
    && docker-php-ext-install gd mbstring opcache pdo pdo_mysql pdo_pgsql zip

# set recommended PHP.ini settings
# see https://secure.php.net/manual/en/opcache.installation.php
RUN { \
        echo 'opcache.memory_consumption=128'; \
        echo 'opcache.interned_strings_buffer=8'; \
        echo 'opcache.max_accelerated_files=4000'; \
        echo 'opcache.revalidate_freq=60'; \
        echo 'opcache.fast_shutdown=1'; \
        echo 'opcache.enable_cli=1'; \
    } > /usr/local/etc/php/conf.d/opcache-recommended.ini

WORKDIR /var/www/html

# https://www.drupal.org/node/3060/release
ENV DRUPAL_VERSION 8.1.9
ENV DRUPAL_MD5 4de7c001ecbd5c27e5837c97e40facc2

RUN curl -fSL "https://ftp.drupal.org/files/projects/drupal-${DRUPAL_VERSION}.tar.gz" -o drupal.tar.gz \
    && echo "${DRUPAL_MD5} *drupal.tar.gz" | md5sum -c - \
    && tar -xz --strip-components=1 -f drupal.tar.gz \
    && rm drupal.tar.gz \
    && chown -R www-data:www-data sites modules themes
docker run --name dp -p 8080:80 -d drupal

2.漏洞分析

首先我們進入后臺把配置文件導出,默認導出到了/tmp/config.tar.gz

Alt text

然后看代碼,我們在core/modules/system/system.routing.yml可以看到這樣一個路由項:

Alt text

這是訪問管理員頁面時的路由,可以看到requirements._permission指定了需要管理員權限。

然后我們再看這一項:

Alt text

可以看到并沒有設置_permission,并且_access=TRUE,也就是說在未授權的情況下是可以訪問這個功能的。

接下來我們跟進它的controller,在core/modules/system/src/FileDownloadController.php第41-68行的download函數:

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)) {
        return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');
      }

      throw new AccessDeniedHttpException();
    }

    throw new NotFoundHttpException();
  }

函數獲取了我們傳入的file參數,與$scheme拼接后檢測文件是否存在。我們訪問 http://xxx/system/temporary/?file=config.tar.gz 然后下斷點動態調試,執行到該函數時各變量的值如下:

Alt text

也就是說$scheme的值是temporary。然后程序進入了file_stream_wrapper_valid_scheme函數檢查協議有效性,動態跟進直到core/lib/Drupal/Core/StreamWrapper/LocalStream.php中第495-506行的url_stat函數:

Alt text

可以看到temporary://config.tar.gz被映射到了/tmp/config.tar.gz,也就是我們剛備份到的位置,所以也就通過了file_exists

通過檢查后回到download函數中,接下來執行了如下語句:

$headers = $this->moduleHandler()->invokeAll('file_download', array($uri));

跟進invokeAll函數,在core/lib/Drupal/Core/Extension/ModuleHandler.php中第397-409行:

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;
      }
    }
  }

動態調試情況如下圖:

Alt text

implementations的值有config, file, image,遍歷這三個值并調用相應函數:

  • config_file_download
  • file_file_download
  • image_file_download

首先調用的是config_file_download,位于core/modules/config/config.module第64-78行:

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,
    );
  }
}

可以看到當且僅當文件是config.tar.gz時設置響應頭以供下載,最后當返回到download函數時,執行如下語句:

return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');

將最終的響應返回給了用戶,從而觸發了下載漏洞。

Alt text

到這里有一個想法,我們可不可以傳入../../etc/passwd\x00config.tar.gz這樣的參數來截斷并且跳到別的目錄呢?

我們看一下在core/lib/Drupal/Core/StreamWrapper/LocalStream.php中第120到144行的getLocalPath()函數,它在上面提到的url_stat函數中被調用:

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

    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;
  }

可以看到路徑中的../realpath過濾,跳出/tmp的目的也就不能達到了。

另外還有一個方面,config_file_download函數比較苛刻,只允許下載config.tar.gz,而image_file_download是為了下載圖片,那么file_file_download函數能否供我們利用以下載系統的敏感文件呢?

file_file_download函數在core/modules/file/file.module中第582-633行:

function file_file_download($uri) {
  // Get the file record based on the URI. If not in the database just return.
  /** @var \Drupal\file\FileInterface[] $files */
  $files = entity_load_multiple_by_properties('file', array('uri' => $uri));
  if (count($files)) {
    foreach ($files as $item) {
      if ($item->getFileUri() === $uri) {
        $file = $item;
        break;
      }
    }
  }
  if (!isset($file)) {
    return;
  }
...
}

有以下三點:

  • 根據注釋可以看到該函數是根據uri來查詢數據庫中的文件記錄再進行下載
  • 我們請求中的$schemetemporary,在file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)限制了我們只能下載/tmp目錄下存在的文件
  • 默認/tmp下除了config.tar.gz只有.htaccess

綜合這三點來看file_file_download函數是不存在下載漏洞的。

所以總的來說該漏洞只能在管理員導出備份的情況下下載/tmp/config.tar.gz

3.補丁分析

Alt text

增加了權限驗證,使未授權用戶不能下載cnofig.tar.gz

0x02 修復方案

升級Drupal到8.1.10

0x03 參考

  • https://www.seebug.org/vuldb/ssvid-92436
  • https://www.drupal.org/SA-CORE-2016-004

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/50/