About the Challenge
The Droops challenge consisted of a website which had a modified version of Drupal 7.63 installed. The creators of the challenge added a Cookie to the Drupal installation that contained a PHP serialized string, which would then be unserialized on the remote server, leading to a PHP Object Injection vulnerability. Finding the cookie was straightforward and the challenge was obvious: Finding and crafting a POP chain for Drupal.
If you are not familiar with PHP Object Injections we recommend reading our blog post about the basics of PHP Object Injections.
Drupal POP Chain to Drupalgeddon 2
We found the following POP chain in the Drupal source code that affects its cache mechanism. Through the POP chain it was possible to inject into the Drupal cache and abuse the same feature that lead to the Drupalgeddon 2 vulnerability. No knowledge of this vulnerability is required to read this blog post, as each relevant step will be explained.
The POP chain is a second-order Remote Code Execution, which means that it consists of two steps:
- Injecting into the database cache the rendering engine uses
- Exploiting the rendering engine and Drupalgeddon 2
Injecting into the cache
DrupalCacheArray class in
includes/bootstrap.inc implements a destructor and writes some data to the database cache with the method
set(). This is our entry point of our gadget chain.
set() method will essentially call Drupal’s
cache_set() function with
$this->bin, which are all under control of the attacker
since they are properties of the injected object. We assumed that we are now able to inject arbitrary data into the Drupal cache.
In order to find out if this assumption was true, we started digging into the internals of the Drupal cache. We found out that the cache entries are stored in the database. Each cache type has its own table. (A cache for forms, one for pages and so on.)
After a bit more of digging around, we discovered that the table name is the equivalent to
$this->bin. This means we can
bin to be of any cache type and inject into any cache table. But what can we do with this?
The next step was to analyze the different cache tables for interesting entries and their structure.
For example the
cache_form table has
a column called
cid. As a reminder, one of the arguments to
$this->cid. We assumed the following:
$this->cid maps to the
cid column of the cache table, which is set in
cid is the key of a cache entry and the
data column simply is the
$data parameter in
To verify all these assumptions we created a serialized payload locally by creating a class in a
build.php file and unserialized it on my test Drupal setup:
The reason we used the
SchemaCache class here is because it extends the abstract class
DrupalCacheArray, which means it can’t be instantiated on its own. The deserialization of this data lead to the following entry in the
cache_form table being created:
Using the injected cached data to gain Remote Code Execution
Since we were now able to inject arbitrary data into any caching table, we started to search for ways in which the cache was used by Drupal that could be used to gain
Remote Code Execution. After a bit of searching, we stumbled upon the following ajax callback, which can be triggered
by making a request to the URL:
ajax_get_form() function internally uses
cache_get() to retrieve a cached entry from the
This is interesting because this means it is possible to pass an arbitrary form render array to
drupal_process_form(). As previously mentioned,
the Drupalgeddon 2 vulnerability abused this feature, so chances were high that code execution could be achieved with the
ability to inject arbitrary render arrays into the rendering engine.
drupal_process_form(), we found the following lines of code:
$element refers to the
$form received via
cache_get(), meaning the keys and values of the array can be set arbitrarily.
This means it is possible to simply set an arbitrary
#process) callback and execute it with the render array as a parameter.
Since the first argument is an array, it is not possible to simply call a function such as
system() directly. What is required is a function
that takes an array as input that leads to RCE.
drupal_process_attached() function seemed very promising:
Since all array keys and values can be set arbitrarily, is is possible to call an arbitrary function with arbitrary arguments via
call_user_func_array(), which leads to RCE!
This means the final POP chain looks like this:
All that is left to do is to trigger the PHP Object Injection vulnerability with the resulting serialized string and then to make a POST request to
http://drupalurl.org/?q=system/ajax and set the POST parameter
1337 to trigger the
POP chains can often become more complex and require a deeper knowledge of the application. However, the purpose of this blog post was to demonstrate that exploitation is still possible, even if no obvious, first order POP chain exists. If we had not known that the rendering API of drupal uses a lot of callbacks and had vulnerabilities in the past, we probably would not have found this particular POP chain. Alternatively, deep PHP knowledge can also lead to working POP chains when no obvious POP chain can be found. There exists another POP chain, an Object Instantion to Blind XXE to File Read to SQL Injection to RCE. A write up for this POP chain was written by Paul Axe and can be found here. We also would like to thank the creators for creating this and the other amazing challenges for the Insomni’hack CTF 2019.