Drupal Leaking Version Information

30 November -0001

Drupal is a robust content management system (CMS) that provides a wide range of functionality for dynamic websites. Drupal is an open source project based on PHP and a database back end. Much of Drupal's power comes from it's modular nature. New functionality can be added and existing functionality can be extended by coding modules that can be plugged into existing Drupal deployments. In fact, much of the Drupal core functionality is supported by modules.

In general, Drupal modules undergo a fairly rigorous security review if they are part of the "core" installation. However, third party modules that are needed to extend Drupal functionality often receive much less attention. The Drupal security announcement mailing list is quite active with reports of flaws discovered in third party modules (and occasionally in core modules). These reports are also available at drupal.org/security.

Developing exploits for known vulnerabilities is fairly trivial by examining the updates in patches and fixes released. In fact, the most difficult task for an attacker when deciding to exploit a Drupal installation, is finding a site that utilizes vulnerable versions of Drupal modules.

Because of it's ease of use and somewhat convoluted architecture under the hood, it is rare for Drupal sites to be properly maintained and upgraded. With Drupal 6's Update status module (part of core) this task is somewhat easier as administrators see alerts in the admin interface whenever updates are available. However, it is still quite common for developers to find modules that suit their needs, install them and forget about them. Because modules cannot be updated via the web interface it is also common for developers to produce Drupal sites then deliver them to clients who have no ability to update out of version modules. Although Drupal security announcements help to broadcast news of a new issue, often these notices don't reach the users of the module. In some cases users may even knowingly opt to disregard security upgrades for fear of disrupting functional systems.

This state of affairs, combined with some fundamental design assumptions within Drupal allows an attacker to easily profile and target Drupal systems for exploitation. By default all Drupal modules (core and third party) are packaged in a directory structure. This directory contains several files that enable the module's functionality. At the very least each module will have a .info and a .module file, but there may be other files in the directory. These files are all named with the same prefix and only the file extension varies. So if I create the module "foo" it would have on directory called foo with, for instance, the following files in it:

foo/
	foo.info
	foo.install
	foo.module

Additionally, each of these files appears in a predictable URL path. Modules are accessible by Drupal only if they are placed in the modules, sites/all/modules, or profiles/profile_name/modules directory under the Drupal root directory. In order to access the file foo.install an attacker simply enumerates http://sample.tld/modules/foo/foo.install, http://sample.tld/sites/all/modules/foo/foo.install or the like. Furthermore, because these files do not contain standard PHP extensions, even the .module file that contains actual PHP code, will be presented by most web servers as plain text.

Drupal protects these files with a .htaccess in it's base directory. This file uses the following code:

# Protect files and directories from prying eyes.

  Order allow,deny

To protect files that might leak versioning information.

However, Drupal will function even if this .htaccess file is not properly installed or if the web server is not configured to allow it to function. For instance, in Apache the Drupal directory needs to have the "AllowOverride All" specification set in the httpd.conf (or similar file). Without this capability enabled Drupal will happily serve up all sorts of damaging information. In a random sampling I found 3 out of 10 surveyed Drupal sites were not properly protected with the .htaccess file and leaked version information.

More reliable methods

Querying Drupal's .info files looking for versioning information may or may not work depending on the implementation of the .htaccess file. A more convenient way to profile the existence of module is to compare a site's response to queries for the .info files. If the file exists and the .htaccess file is properly configured then the site will typically respond with a 403 error for access denied. If the module does not exist then the server should respond with a 404 error for 'file not found.'

[justin@localhost tmp]$ wget http://172.16.46.129/drupal-6.16/sites/all/modules/foo/foo.info
--2010-05-24 08:36:01--  http://172.16.46.129/drupal-6.16/sites/all/modules/foo/foo.info
Connecting to 172.16.46.129:80... connected.
HTTP request sent, awaiting response... 404 Not Found
2010-05-24 08:36:01 ERROR 404: Not Found.

[justin@localhost tmp]$ wget http://172.16.46.129/drupal-6.16/sites/all/modules/diff/diff.info
--2010-05-24 08:36:18--  http://172.16.46.129/drupal-6.16/sites/all/modules/diff/diff.info
Connecting to 172.16.46.129:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2010-05-24 08:36:18 ERROR 403: Forbidden.

In this example you can see that no module named 'foo' exists, but the diff module is in fact installed. Even though access is denied to the .info file there are other ways of querying the version information. Because .txt and .css files are not excluded by Drupal's .htaccess it is possible to retrieve those two files directly from the server. By taking the MD5 hash of the file we retrieved from the site we can compare it to various MD5 hashes of the same file from different versions of the module release:

[justin@localhost tmp]$ wget http://172.16.46.129/drupal-6.16/sites/all/modules/diff/diff.css
--2010-05-24 08:48:45--  http://172.16.46.129/drupal-6.16/sites/all/modules/diff/diff.css
Connecting to 172.16.46.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 974 [text/css]

[justin@localhost tmp]$ md5sum diff.css
f0a4a622d23be3cb1994dd5b1a14db34  diff.css
[justin@localhost tmp]$ md5sum diff-6.x-2.0/diff.css
f0a4a622d23be3cb1994dd5b1a14db34  diff-6.x-2.0/diff.css
[justin@localhost tmp]$ md5sum diff-6.x-1.0/diff.css
f0a4a622d23be3cb1994dd5b1a14db34  diff-6.x-1.0/diff.css
[justin@localhost tmp]$ md5sum diff-6.x-2.1-alpha1/diff.css
721e6116cd46754970aaa080f75159bc  diff-6.x-2.1-alpha1/diff.css

Looking at the above output we can see that the md5sum of the diff.css file we retrieved from the target was 'f0a4a622d23be3cb1994dd5b1a14db34', which matches the 6.x-2.0 and 6.x-1.0 releases, but not the 6.x-2.1-alpha1 release. Looking at the .txt files we see:

[justin@localhost tmp]$ wget http://172.16.46.129/drupal-6.16/sites/all/modules/diff/readme.txt
--2010-05-24 08:50:38--  http://172.16.46.129/drupal-6.16/sites/all/modules/diff/readme.txt
Connecting to 172.16.46.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 871 [text/plain]

[justin@localhost tmp]$ md5sum readme.txt 
18f24b6b23e930f5fa4e311d118b39de  readme.txt
[justin@localhost tmp]$ md5sum diff-6.x-2.0/readme.txt 
18f24b6b23e930f5fa4e311d118b39de  diff-6.x-2.0/readme.txt
[justin@localhost tmp]$ md5sum diff-6.x-2.1-alpha1/readme.txt 
89b44545a128aaa17bfc3222d0538c05  diff-6.x-2.1-alpha1/readme.txt
[justin@localhost tmp]$ md5sum diff-6.x-1.0/readme.txt 
3d117769f562ff8f53e2d29e3d59ccbf  diff-6.x-1.0/readme.txt

Note that the MD5 hash of the readme file matches only the 6.x-2.0 version of the module. Thus, we can conclude that the version of the module installed is the 6.x-2.0 version.

Examining the .info directly

By requesting code from the modules directory an attacker can enumerate the exact code used to power a particular module (as well as confirming it's existence) as well as determine the module version. The version info is usually presented as the very first line in each Drupal module file. For instance, the foo.info file might begin:

; $Id: foo.info,v 1.3 2006/11/21 20:55:33 dries Exp $
name = Foo
description = "Foo is an example module."

By pulling up these files and browsing their content, an attacker can easily profile what modules are installed on a Drupal server, and capture their versions. For instance, if we create a text file called modules.txt that lists a few sample modules such as:

aggregator
block
blog
blogapi
book
color
comment
contact
contemplate
date
drupal
filter
forum
help
legacy
locale
menu
node
path
ping
poll
profile
search
statistics
system
taxonomy
throttle
tracker
upload
user
views
watchdog

We can easily craft a Perl script that will enumerate the versions of each of those modules for us like so:

#!/usr/bin/perl
#
# Drupal Snooper
#
#  Purpose:  This script finds the versions of modules installed on a Drupal site
#  Author:  Justin C. Klein Keane 
#  Last Modified: July 7, 2008

use LWP::Simple;

my $mfile = 'modules.txt';
open(FILE, $mfile) or die('Could not open the list of modules');
my @modules = ;
close(FILE);

if (! $ARGV[0]) {
  usage();
}
my $target = $ARGV[0];

foreach $module (@modules) {
  chomp $module;
  $url = $target . "/modules/" . $module . "/" . $module . ".info";
  $page = get($url);
  $version = substr($page, 0, index($page, 'Exp $')+5);
  if (! length($version)) { print "Module $module not installed.\n"; }
  else { print "Version info for module $module : " . $version . "\n"; }
}

sub usage {
  print("Usage:  drupal_snooper.pl  [target site]\n");
  exit 0;
}

Further modifying this script to differentiate between 403 and 404 errors we can easily profile a site, and even get information for what modules are installed and what modules are not. It's relatively easy to get a complete list of Drupal modules. Once armed with a list of potential targets an attacker could profile each of the sites then target only sites with known vulnerable versions of the code installed.

Compiling a list of Drupal sites is a relatively simple task. Drupal uses the 'node' keyword in URL's and has other signatures that could easily be plugged into search engines in order to get a fairly large list of potential victim Drupal instances. Picking out other common text strings (such as "Powered by Drupal") is also effective in finding sites.

How to Defend Yourself

If you're running Drupal it is critical to make sure that your .htaccess is working properly. Using the 'Clean URL test' link in the clean URL section of the site (Home -> Administer -> Site Configuration -> Clean URLs or ?q=admin/settings/clean-urls) is the easiest way to test and ensure that your .htaccess is working properly. If you find that clean URLs won't work for some reason there may be cause for concern. Try to navigate to the one of the files in the modules directory and see if your webserver presents the content. If it does you need to take steps to enable your .htaccess protection or otherwise hide these files from prying eyes. Removing unnecessary text files is helpful, but as we have seen it is not foolproof. Because module style sheets (.css) are required for proper function it is impossible to deny access to these files. However, as observed above, without the additional text files it may be difficult, or impossible, to determine the exact version of modules installed. A much more effective approach is to simply keep all installed modules up to date. Subscribe to the Drupal security announcement mailing list and monitor new developments closely.