作者:RicterZ@云鼎實驗室

漏洞分析

Drupal 在 3 月 28 日爆出一個遠程代碼執行漏洞,CVE 編號 CVE-2018-7600,通過對比官方的補丁,可以得知是請求中存在 # 開頭的參數。Drupal Render API 對于 # 有特殊處理,比如如下的數組:

$form['choice_wrapper'] = array(
  '#tree' => FALSE, 
  '#weight' => -4, 
  '#prefix' => '<div class="clearfix" id="poll-choice-wrapper">', 
  '#suffix' => '</div>',
);

比如 #prefix 代表了在 Render 時元素的前綴,#suffix 代表了后綴。

通過查閱 Drupal 的代碼和文檔,可以知道,對于 #pre_render#post_render#submit#validate 等變量,Drupal 通過 call_user_func 的方式進行調用。

在 Drupal 中,對于 #pre_render 的處理如下:

// file: \core\lib\Drupal\Core\Render\Renderer.php
if (isset($elements['#pre_render'])) {
    foreach ($elements['#pre_render'] as $callable) {
      if (is_string($callable) && strpos($callable, '::') === FALSE) {
        $callable = $this->controllerResolver->getControllerFromDefinition($callable);
      }
      $elements = call_user_func($callable, $elements);
    }
  }

所以如果我們能將這些變量注入到 $form 數組中,即可造成代碼執行的問題。

但是由于 Drupal 代碼復雜,調用鏈很長,所以導致了所謂“開局一個 #,剩下全靠猜”的尷尬局面,即使知道了漏洞觸發點,但是找不到入口點一樣尷尬。直到昨日,CheckPoint 發布了一篇分析博客,我才注意到原來 Drupal 8.5 提供了 Ajax 上傳頭像的點,并且明顯存在一個 $form 數組的操縱。在已經知道觸發點的情況下,構造剩下的 PoC 就非常容易了。

PoC 構造

CheckPoint 提供的截圖顯示,是在 Drupal 8.5.0 注冊處,漏洞文件為:\core\modules\file\src\Element\ManagedFile.php,代碼如下:

public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
  /** @var \Drupal\Core\Render\RendererInterface $renderer */
  $renderer = \Drupal::service('renderer');

  $form_parents = explode('/', $request->query->get('element_parents'));

  // Retrieve the element to be rendered.
  $form = NestedArray::getValue($form, $form_parents);

  // Add the special AJAX class if a new file was added.
  $current_file_count = $form_state->get('file_upload_delta_initial');
  if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
    $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
  }
  // Otherwise just add the new content class on a placeholder.
  else {
    $form['#suffix'] .= '<span class="ajax-new-content"></span>';
  }

  $status_messages = ['#type' => 'status_messages'];
  $form['#prefix'] .= $renderer->renderRoot($status_messages);
  $output = $renderer->renderRoot($form);

代碼第五行,取出 $_GET["element_parents"] 賦值給 $form_parents,然后進入 NestedArray::getValue 進行處理:

public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
  $ref = &$array;
  foreach ($parents as $parent) {
    if (is_array($ref) && (isset($ref[$parent]) || array_key_exists($parent, $ref))) {
      $ref = &$ref[$parent];
    }
    else {
      $key_exists = FALSE;
      $null = NULL;
      return $null;
    }
  }
  $key_exists = TRUE;
  return $ref;
}

NestedArray::getValue 函數的主要功能就是將 $parents 作為 key path,然后逐層取出后返回。舉個例子,對于數組:

array(
  "a" => array(
    "b" => array(
      "c" => "123",
      "d" => "456"
    )
  )
)

$parentsa/b/c,最后得到的結果為 456

查看一下在正常上傳中,傳入的 $form

似乎 #value 是我們傳入的變量,嘗試注入數組:

發現成功注入:

那么通過 NestedArray::getValue 函數,可以傳入 element_parentsaccount/mail/#value,最后可以令 $form 為我們注入的數組:

在 Render API 處理 #pre_render 時候造成代碼執行:

Exploit 構造

雖然實現了代碼執行,但是 #pre_render 調用的參數是一個數組,所以導致我們不能任意的執行代碼。不過 Render API 存在很多可以查看的地方,通過翻閱 Renderer::doRender 函數,注意到 #lazy_builder

  $supported_keys = [
    '#lazy_builder',
    '#cache',
    '#create_placeholder',
    '#weight',
    '#printed'
  ];
  $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
  if (count($unsupported_keys)) {
    throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
  }
}
...
// Build the element if it is still empty.
if (isset($elements['#lazy_builder'])) {
  $callable = $elements['#lazy_builder'][0];
  $args = $elements['#lazy_builder'][1];
  if (is_string($callable) && strpos($callable, '::') === FALSE) {
    $callable = $this->controllerResolver->getControllerFromDefinition($callable);
  }
  $new_elements = call_user_func_array($callable, $args);
  ...
}

#lazy_builder 是一個 array,其中元素 0 為函數名,元素 1 是一個數組,是參數列表。接著利用 call_user_func_array 進行調用。不過注意到上方這段代碼:

$unsupported_keys = array_diff(array_keys($elements), $supported_keys);

意思為傳入的 $elements 數組中不能存在除了 $supported_keys 之外的 key,常規傳入的數組為:

比要求的數組多了 #suffix#prefix。不過 Render API 有 children element 的說法:

// file: \core\lib\Drupal\Core\Render\Element.php
public static function children(array &$elements, $sort = FALSE) {
  ...  
  foreach ($elements as $key => $value) {
    if ($key === '' || $key[0] !== '#') {
      if (is_array($value)) {
        if (isset($value['#weight'])) {
          $weight = $value['#weight'];
          $sortable = TRUE;
        }
        else {
          $weight = 0;

當數組中的參數不以 # 開頭時,會當作 children element 進行子渲染,所以我們傳入 mail[a][#lazy_builder] ,在進行子渲染的過程中,就會得到一個干凈的數組,最終導致命令執行。

其他版本

本文分析的是 Drupal 8.5.0,對于 8.4.x,在注冊時默認沒有上傳頭像處,但是也可以直接進行攻擊,對于 Drupal 7,暫時未找到可控點。

Reference

https://research.checkpoint.com/uncovering-drupalgeddon-2/


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