Auditing WP-DB-Backup WordPress Plugin & Why Using the Database Password for Entropy is a Bad Idea

After installing the WordPress plugin “WP-DB-Backup” found at http://wordpress.org/plugins/wp-db-backup/ I saw some insecure looking practices being taken when it came to storing the created backups. At the time of this writing there is just over two million downloads of this plugin and it has a rating of 3.8/5 stars. The reason I’m posting this however, is because it has some interesting security issues that I’d like to share.

Checking for a WP-DB-Backup Install

**You can find out if a site is running this software by probing for the existence of certain files in the /wp-content/plugins/wp-db-backup/ folder.

The files you can probe for are as follows:

LICENSE 
README.markdown 
readme.txt 
wp-db-backup-ar.mo 
wp-db-backup-ar.po 
wp-db-backup-ca.mo 
wp-db-backup-ca.po 
wp-db-backup-de_DE.mo 
wp-db-backup-de_DE.po 
wp-db-backup-el.mo 
wp-db-backup-el.po 
wp-db-backup-es_ES.mo 
wp-db-backup-es_ES.po 
wp-db-backup-fa_IR.mo 
wp-db-backup-fa_IR.po 
wp-db-backup-fr_FR.mo 
wp-db-backup-fr_FR.po 
wp-db-backup-it_IT.mo 
wp-db-backup-it_IT.po 
wp-db-backup-ja.mo 
wp-db-backup-ja.po 
wp-db-backup-ko_KR.mo 
wp-db-backup-ko_KR.po 
wp-db-backup-nb_NO.mo 
wp-db-backup-nb_NO.po 
wp-db-backup.php 
wp-db-backup.pot 
wp-db-backup-pt_BR.mo 
wp-db-backup-pt_BR.po 
wp-db-backup-ru_RU.mo 
wp-db-backup-ru_RU.po 
wp-db-backup-sv_SE.mo 
wp-db-backup-sv_SE.po 
wp-db-backup-tr_TR.mo 
wp-db-backup-tr_TR.po 
wp-db-backup-zh_CN.mo 
wp-db-backup-zh_CN.po

Storage of Backups

TL;DR if they are using a guessable database name/prefix you can enumerate the .sql backups – but it’ll cost you quite a few HTTP requests per try.

The plugin takes a backup of your WordPress database and exports it as a flat .sql file to restore from later. You can export the backups to your local computer, over email, or just simply stored on the web server. Backing up to the web server is discouraged in the readme.txt file:

Although WP-DB-Backup provides the option of saving the backup file to the server, I strongly discourage anyone from leaving backed-up database files on the server. If the server is not perfectly configured, then someone could gain access to your data, and I do not want to make it easy for that to happen.

Luckily, nobody actually reads the readme.txt, considering most people are just using WordPress to install the plugin. This is a good warning, but even a perfectly configured server could be danger due to insufficient entropy being used in the creation of SQL backups.

The backups are stored in a “randomly” generated folder inside of “wp-contents”. I put “randomly” in quotes because this is how that folder name is generated:

$rand = substr( md5( md5( DB_PASSWORD ) ), -5 );
...
define('WP_BACKUP_DIR', $wpdbb_content_dir . '/backup-' . $rand . '/');

Yes, the folder is generated based off of the last five characters of the WordPress database password (after double MD5ing it for super security).

So we have to bruteforce these five hexadecimal digits – what’s the math on that? Since our keyspace is any hex character and we have a total of five digits we have 16^5 possibilities or 1,048,576 permutations. That’s not too bad but how do we know when we’ve gotten the correct folder? Luckily for us we have a blank index.php file inside this directory. If we get an HTTP 200 OK instead of a 404 it means we’ve hit paydirt. That’s still a pretty time consuming key space, but certainly not impossible to enumerate. Assuming we can do ~100 requests a second it would take us just under three hours to crack this directory name.

Once we have the folder we are closer to obtaining the SQL backups we crave. Are these SQL backup filenames random as well? Not exactly, the file names are generated like the following:

$datum = date("Ymd_B");
$this->backup_filename = DB_NAME . "_$table_prefix$datum.sql";

Yuck, this may present a problem. The variables are as follows:

DB_NAME: The WordPress database name (this is the tricky part)

$table_prefix: The WordPress database prefix (default “wp_” )

$datum: Generated via PHP date “Ymd_B” – that’s a four digit year (0000), a two digit month (00), a two digit day (00), and a three digit Swatch Internet Time (000).

With a bit of intelligence in our approach we can get the $datum field down to a much shorter keyspace. For example, say we knew the year but nothing else – what would the number of permutations be? We can rule out quite a bit of keyspace, there is never a month over 21, never a day over 31, and never a swatch time over 999.

The math is: 12 * 31 * 999 = 371,628 permutations. This is a round up considering all months don’t have 31 day but still not a large key space.

Assuming 371,628 attempts for each year tested, any WordPress blog could be bruteforcable if we know the table prefix and database name.

Those Enumerated Hex Digits

EDIT: As an important side note the directory for backups is created before any backups are ever made. This means these hex characters are leaked by just installing the plugin.

So assume we couldn’t find the .sql backups because we don’t know the database name or prefix. We still have those neat tailing characters of the password. Can five leaked characters really help us that much?

You’d be surprised what five characters can do!

Say for example our last five characters of the database password are “17c82”. How much does this narrow down our password list?

For this example I’ve created a quick proof of concept in PHP:

<?php
$target = "17c82";
$fp = fopen( 'rockyou.txt', 'r' );
if( $fp ) {
    while( ( $line = fgets( $fp ) ) !== false ) {
        $tail = substr( md5( md5( trim($line) ) ), -5 );
        if ( $tail === $target ) {
            echo trim( $line )."\n";
        }
    }
}
fclose( $fp );

We will be using the infamous “rockyou.txt” wordlist for this demonstration, the script simply takes each password, calculates the double MD5 hash, and checks for those last five characters. If the password is found then it is printed to the screen for us as a possible password. The rockyou.txt wordlist has a total of 14,344,391 passwords in it, how many can we shrink that down to?

After running the script we get the following output:

mandatory@mandatorys-box ~/P/c/password_lists> php poc.php 
sexy42 
09104913264 
totallysexy 
sarra12 
mjhdh1964 
mike32165 
marymary52 
magandaga 
esbcjb 
dn12ntleo* 
deidrch 
carinomacam 
canthack 
Pumbathepig. 
N0f8ar 
2120983 
147131 
096529540 
0896260024 
0853479846 
mandatory@mandatorys-box ~/P/c/password_lists>

For those counting at home we just went from 14,344,391 passwords to just 20 – not bad at all! The actual password was “canthack” which luckily was in rockyou.txt. If this proof of concept isn’t convincing enough I encourage you to modify this PoC to test on your own wordlist. Lets just pray that they don’t reused the database password on the login (or anywhere else)!

The reverse could also be done, to enumerate the directory it might just be easier to take a list of common passwords and calculate the double MD5 for them. You’d then use these last five hex digits as possible directory names for enumeration.

Obviously, I’d highly recommend people to not use this plugin. At least there is no obvious Google dork to enumerate every site running it.

-mandatory

Matthew Bryant (mandatory)

Matthew Bryant (mandatory)
Security researcher who needs to sleep more. Opinions expressed are solely my own and do not express the views or opinions of my employer.