Background

SA-CORE-2012-003 fixes an issue in the Drupal installer that enables an attacker to cause the site to use a different attacker-controlled database. This database can be an external server or an SQLite file. The vulnerability also causes the installer to leak database information such as the database type, name, host and the username used to connect to the database. The only item not leaked is the database password.

The installer vulnerability was found while preparing my DrupalJam presentation (NL) on security audits and reported via the SecuriTeam Secure Disclosure program (also via ssd@beyondsecurity.com). As promised on IRC & Reddit, here's some additional information on the root cause(s).

Installer system

The vulnerability is caused by an assumption of the install system combined with a bug. The assumption is that errors generated while contacting the database indicate a system that still needs installation. To aid in understanding the additional bug, some global information on the installer follows first.

The problem revolves around the function install_begin_request . It verifies the validity of the current settings.php via install_verify_settings , then determines what the install state is in order to decide if installation should continue or if the site has already been installed.

Here’s the function (elided for brevity):

function install_begin_request ( & $install_state ) {

// ...

$install_state [ 'settings_verified' ] = install_verify_settings ( ) ; install_begin_requestinstall_verify_settings if ($install_state['settings_verified']) {

// Initialize the database system. Note that the connection

// won't be initialized until it is actually requested.

require_once DRUPAL_ROOT . '/includes/database/database.inc'; // Verify the last completed task in the database, if there is one.

$task = install_verify_completed_task();

}

else {

$task = NULL; // Since previous versions of Drupal stored database connection information

// in the 'db_url' variable, we should never let an installation proceed if

// this variable is defined and the settings file was not verified above

// (otherwise we risk installing over an existing site whose settings file

// has not yet been updated).

if (!empty($GLOBALS['db_url'])) {

throw new Exception(install_already_done_error());

}

} // Modify the installation state as appropriate.

$install_state['completed_task'] = $task;

$install_state['database_tables_exist'] = !empty($task);

}

When install_verify_settings does not succeed in verifying settings.php the else path is taken, and installation will proceed, provided there's no global db_url set (this global indicates a D6 settings.php and marks a system that is to be updated).

install_verify_settings will ultimately run db_run_tasks (a wrapper for the DatabaseTasks->runTasks method). The tasks executed in db_run_tasks are listed in install.inc in the abstract class DatabaseTasks and consist of table creation, insertion, deletions and dropping.

Any exceptions generated during db_run_tasks will be reported as errors to install_verify_settings . This means that when errors of any kind (eg database is down) are generated by the db, the settings.php file fails verification and installation will proceed by displaying a partially filled in Database configuration page.

If required information is send in a POST request, the installer will mark settings.php as writable (if possible) and then update settings.php with this information.

Bug

Being able to do a new install when the database is down, or settings.php is temporarily broken is bad enough, but not what makes SA-CORE-2012-003 so grave. The big problem is the possibility to force the verifier to error out on a working database.

To see how, let's look at the database tasks install_verify_settings runs:

CREATE TABLE {drupal_install_test} (id int NULL)

…

DROP TABLE {drupal_install_test}

You probably see the problem now. If not, consider two POST requests fired nearly simultaneously at install.php:

Request 1 executes the db-task CREATE TABLE {drupal_install_test} (id int NULL) ; The table will be created.

; The table will be created. Request 2 executes the db-task CREATE TABLE {drupal_install_test} (id int NULL) ; Oops…

; Oops… Request 1 executes the rest of the db-tasks and then deletes drupal_install_test

Trying to create an already existing table in Request 2 will generate an exception that will be reported as a db/settings error to install_begin_request and thus a signal for a continued installation. By sending the right information in the POST request, a settings.php is written that points to the attacker controlled database.

Notes

I'm comfortable releasing this information after a week, because the mitigation step is pretty easy and without consequence: delete install.php or block access to it. If you didn't upgrade or follow the mitigation steps, do so now, then review your update procedures. I conciously did not provide a weaponized example of the exploit; While it's easy to create such an exploit based on this article, it is a step many attackers cannot or will not take.

Should you learn of a serious vulnerability in Drupal or another piece of software, consider the SecuriTeam Secure Disclosure program (also via ssd@beyondsecurity.com). I had a great experience.