views:

704

answers:

2

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.'

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 ]...
+12  A: 

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.

Tom Ritter
an empty array will return true for an empty() check, but by passing key[]= you get an array with an empty string as its only element.vpsprintf() returns a value, rather than outputting one so your sample code isn't really telling anything. In both cases, your reasoning is sound and the end result is the same.
Greg
Added your corrections
Tom Ritter
Thanks for your analysis!
PaulG
It's worth noting this exploit doesn't affect older Wordpress installs, and only became viable with a recent [change](http://core.trac.wordpress.org/changeset/10721) to `prepare`
searlea
A: 

I have a related question on how to patch this vulnerability - line 190 on the wp-login.php should now look like this;

if ( empty( $key ) || is_array( $key ) )
pageman