Drupal insecure default leads to password reset poisoning

What is Drupal?

Drupal is a free and open-source web content management framework written in PHP. Drupal provides a back-end framework for at least 13% of the top 10,000 websites worldwide – ranging from personal blogs to corporate, political, and government sites according to Wikipedia. For this test we used the latest version of Drupal with the default configuration.

This vulnerability was discovered using a blackbox approach. However we also performed a source code review & dynamic analysis in order to determine the root cause of the vulnerability. We have reached out to the Drupal team to disclose and allow them time to fix, however it seems that this is a known issue and that a fix is documented, which we will discuss later . The Drupal team was very cooperative and allowed us to publish our findings. As you probably guessed this is a password poisoning vulnerability, which allows us to reset an administrator’s password. There is an interesting behaviour in the vulnerability which we will discuss in the Source Code analysis section together with the limitations of the current solution.

The vulnerability

On the ‘Reset your password’ page, enter the victim’s email and click submit.

Intercept the request, modify the Host header and send the request:

Right afterwards, in the app, the user will be redirected to the attacker’s domain as seen below:

In addition, you will get the email with the poisoned ‘reset password’ link:

If the victim clicks the above link, the reset token gets sent to the ‘attacker.com’ domain and thus the attacker can reset victim’s password (which be an admin).

Source code analysis

The password reset process is triggered inside the _user_email_notify() function:

The password reset token is generated inside the user_mail_tokens():


The password reset url that gets sent in the notification email is created in the user_pass_reset_url() function, by the Url::fromRoute()->toString() function as can be seen bellow:

The Url->toString() method will call urlGenerator()->generateFromRoute()

And generateFromRoute() does the interesting part where it gets the Host from $this->context->getHost() method.

Dynamic Analysis

So now, the question becomes where is the Host actually set? We tried to find out doing source code review where the Host property is set for the $context(RequestContext class) is set, but without much success. So it’s time to set up a debugger and do some dynamic analysis.

The Host property of the RequestContext class is set in the RequestContext constructor and also in the “fromRequest()” method of the same class. What is really interesting is that the constructor is called first with a safe value (“localhost” in this case) and then the fromRequest() method is called which overrides the safe value with the poisoned host header:

After the constructor is called, the default value is overridden by a malicious header controlled by the attacker, thus making the password reset process vulnerable:

Thus, an attacker can control the host header of the password reset link and capture the token in order to reset an administrator’s password. Once an admin, then the attacker can upload a custom Drupal module which will give him RCE on the server.

Disclosure

This finding has been disclosed to Drupal team. However, their answer is that this is a known issue and it’s documented here https://www.drupal.org/docs/installing-drupal/trusted-host-settings. What is even more amazing to us is that it’s a known issue since 2014 https://www.drupal.org/project/drupal/issues/2221699 and it still hasn’t been patched.

As demonstrated above this approach is vulnerable and the documented fix has some serious limitations:

  • it’s not secure by default
  • there were no warnings raised during the setup process
  • it puts the onus on Drupal Admins which might be non technical people and it’s expected they know how to write secure regexps
  • as explained in the Source Code Analysis section the link is is initially constructed securely and then the host header gets overridden with the attacker controlled host
  • this can result in RCE on the server, because the Admin can upload custom Drupal modules

References
Burpsuite: https://portswigger.net/web-security/host-header/exploiting/password-reset-poisoning