Understanding the WordPress vulnerability

A vulnerability has recently been disclosed that affects WordPress 2.8.3 and allows the admin user to be locked out of their account by changing the password.

This post on Full Disclosure details the flaw, and includes relevant code snippets. The post mentions that ‘You can abuse the password reset function, and bypass the first step and then reset the admin password by submiting an array to the $key variable.’

Read More

I’d be interested in someone familiar with PHP explaining the bug in more detail.

Those affected should update to a new 2.8.4 release which apparently fixes the flaw.

wp-login.php:
...[snip]....
line 186:
function reset_password($key) {
    global $wpdb;

    $key = preg_replace('/[^a-z0-9]/i', '', $key);

    if ( empty( $key ) )
        return new WP_Error('invalid_key', __('Invalid key'));

    $user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->users WHERE
user_activation_key = %s", $key));
    if ( empty( $user ) )
        return new WP_Error('invalid_key', __('Invalid key'));
...[snip]....
line 276:
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : 'login';
$errors = new WP_Error();

if ( isset($_GET['key']) )
    $action = 'resetpass';

// validate action so as to default to the login screen
if ( !in_array($action, array('logout', 'lostpassword', 'retrievepassword',
'resetpass', 'rp', 'register', 'login')) && false ===
has_filter('login_form_' . $action) )
    $action = 'login';
...[snip]....

line 370:

break;

case 'resetpass' :
case 'rp' :
    $errors = reset_password($_GET['key']);

    if ( ! is_wp_error($errors) ) {
        wp_redirect('wp-login.php?checkemail=newpass');
        exit();
    }

    wp_redirect('wp-login.php?action=lostpassword&error=invalidkey');
    exit();

break;
...[snip ]...

Related posts

Leave a Reply

2 comments

  1. So $key is an array in the querystring with a single empty string [”]

    http://DOMAIN_NAME.TLD/wp-login.php?action=rp&key[]=

    reset_password gets called with an array, and then preg_replace gets called:

     //$key = ['']
     $key = preg_replace('/[^a-z0-9]/i', '', $key);
     //$key = [''] still
    

    because preg_replace accepts either a string or an array of strings. It regex replaces nothing and returns the same array. $key is not empty (it’s an array of an empty string) so this happens:

     $user = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->users 
          WHERE user_activation_key = %s", $key));
    

    Now from here, I need to go read the wordpress source for how prepare behaves…

    More:

    So prepare calls vsprintf which produces an empty string

    $a = array('');
    $b = array($a);
    vsprintf("%s", $b);
    //Does not produce anything
    

    So the SQL is:

    SELECT * FROM $wpdb->users WHERE user_activation_key = ”

    Which will apparently match the admin user (and all users without activation_keys I suppose).

    And that’s how.