作者: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"
)
)
)
及 $parents
:a/b/c
,最後得到的結果為 456
。
查看一下在正常上傳中,傳入的 $form
: