Drupal Code Execution Vulnerability Analysis

Drupal Code Execution Vulnerability Analysis

March 30, 2018 | Adeline Zhang

Recently, Drupal, a popular open-source content management framework, is found to contain a highly critical remote code execution vulnerability, which allows attackers to execute malicious code on a Drupal site, resulting in the site being completely compromised. This vulnerability is assigned CVE-2018-7600.

The root cause of this vulnerability is related with Drupal’s rendering of forms:

Drupal provides an application programming interface (API) to generate, validate, and process HTML forms. The form API abstracts a form into a nested array, which contains attributes and values. When generating a page, the form rendering engine renders the array at an appropriate time. This means that:

“We do not directly produce an HTML page, but create an array and make the engine generate the HTML page.” As the representation of forms is processed as structured data, it is possible to add, delete, re-sort, and modify forms. If you want to modify forms created by other modules effortlessly, this is a convenient method.Source: http://www.thinkindrupal.com/book/export/html/1100

Obviously, in Drupal, we do not need to directly produce an HTML form, but create an array first. The form rendering engine constructs a form with the name of $form by using the buildForm method in the \drupal\core\lib\Drupal\Core\Form\FormBuilder.php file and then renders the corresponding HTML form.

From the definition of buildForm given below, we can see that it is used to build forms.

The final form is shown as below:

This is where the vulnerability exists.

For an application built on the Drupal framework, background form arrays have been written by developers, like the following:

Attackers cannot change the key values of form array elements.

Many applications provide a convenient method as follows:

Assume that you want to register an account. For this purpose, you need to type the user name, password, email address, and telephone number. After you click Submit, the website prompts that the user name already exists.

Then you find that you do not need to type the password, email address, and telephone number again as the page has saved this information.

Drupal also has this function. How does it implement this function? Let’s do an experiment:

First we submit a normal form.

Then we insert a breakpoint at the return line of the buildForm function.

Finally, we complete and submit the form.

We are redirected to the page that prompts registration success.

The breakpoint inserted at the return line of the buildForm function does not work.

Then we attempt to register an account with the same information:

This time the programs stops at the breakpoint:

At this breakpoint, we change the name value to kingsguard_test_1.

Then the following page is displayed:

The process is as follows:

  1. A user fills out a form à The form is valid à The user is redirected to the page prompting registration success.
  2. A user fills out a form à The form is invalid (for example, the user name already exists) à The buildForm method is invoked to build user-supplied content into a form array à The form array is rendered into an HTML page, which is then returned.

Just now we changed the name value at the breakpoint from kingsguard to kingsguard_test_1, so Username on the returned page is displayed as kingsguard_test_1.

Now we have the kill chain. Attacker-supplied values are used to build a form array with the buildForm method and this form array is then parsed by Drupal’s form rendering engine into an HTML page.

To upload a picture to this registration page,

we need to send the following request:

After being successfully uploaded, the picture is displayed as a thumbnail on the registration page, as shown in the following figure.

This thumbnail has been parsed with the uploadAjaxCallback method in the drupal\core\modules\file\src\Element\ManagedFile.php file.

Let’s look back on the buildForm method. After producing the $form array, buildForm passes it to the uploadAjaxCallback method for parsing, with a view to displaying the picture as a thumbnail on the registration page.

Now that we are clear about the process, we can construct a proof of concept (PoC) to demonstrate it. First, send the following data by using the POST method:

The buildForm function is invoked to build a form array ($form), which is then passed to the uploadAjaxCallback method.

Have a look at the uploadAjaxCallback method:

The $form variable passed to the uploadAjaxCallback method is the form array built with the buildForm method.

After the $form array is passed to the uploadAjaxCallback method, we notice a line, as shown in the red box in the following figure.

Surprisingly, $form_parents is passed from GET! This indicates that this variable is manipulable. In fact, it maps to “element_parents=account/mail/%23value” in our PoC.

The following figure shows the expanded $form_parents:

After $form_parents and $form are processed with the NestedArray::getValue method, result values are assigned to $form.

The new $form variable is as follows:

Let’s move on to the renderRoot method.

The $form variable passed to renderRoot is as follows:

Look into the renderRoot method:

The render method is invoked.

Look further into the render method:

The doRender method is invoked.

As for the doRender method, in line 505,

the call_user_func method is invoked.

Parameters are as follows:

Where,

$callable=”exec”

$elements[‘#children’]=”kingsguard_text” (This is the malicious code we passed previously. The related operation is omitted here.)

Conclusion

In my opinion, this vulnerability is caused by two small issues. First, buildForm does not restrict user-supplied variables, making it possible to pass such variables as mail[#post_render] and mail[#type]. This issue alone, however, does not pose a serious threat because, for the finally rendered HTML page, arrays passed are still arrays, without being parsed as elements. The problem is that the $form_parents variable in the uploadAjaxCallback method is directly retrieved from get(‘element_parents’). This, coupled with the first issue, misleads $form_parents into taking the previously passed values as elements, hence the vulnerability in question.