WordPress vulnerability: Remote admin password reset

Today, a way was presented how to reset an admin’s password of a WordPress installation, by just calling http://domain.dom/wp-login.php?action=rp&key[]=

So please, as long as there is no official release fixing this problem, apply this changeset to your wp-login.php.

Simply change line 190 in wp-login.php to

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

Then the “arraytrick” does not work anymore. The trick was, that after bypassing if (empty($key)), the database is queried for all users having a blank user_activation_key field. This is true for all users by default (except for those, who have recently ordered an activation key for password reset). Hence, the database simply returns the first user, whose user_activation_key is empty. His password then is reset. This user is likely the admin, because he is the first user in the table.

Update:

The changeset named above is not the only change the WordPress developers made. As we can see from changeset 11800 and changeset 11801, the password reset is only done when the key is actually a string and the user calling the “reset password URL” is logged in. Both modifications are already branched, so you can take this wp-login.php or wait for the next official release.

Update2:

The official update to WordPress 2.8.4 is released! Update now!

  • Dave

    Can you give a little more context as to where in the code this should be placed in case we have variations on wp-login.php?

  • Dave,

    of course:

    just look for the following lines:

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

    Then replace the conditional statement with the one I mentioned in the blog post:

        if ( empty( $key ) || is_array( $key ) )
            return new WP_Error('invalid_key', __('Invalid key'));
  • Thanks very much. Patched!

  • Now that I look at the code.. is this necessary?

    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'));

    It seems that while it bypasses the first condition, it won’t return a user and then throw an WP_Error, no?

  • John, this is not totally clear to me, too, because I never looked into WordPress deeply. But it seems that exactly this point is the clue; instead of not returning a user, the admin user seems to be returned in case of key=[]. Weird. To know for sure, one has to know the database structure. If you have a better explanation, then let us know!

    Btw: You can use the

    
    

    tag to get beautiful code! I will quickly edit your comment, hopyfully that's okay for you :-)

  • John, the explanation is pretty simple. I found this in the wp-hackers mainlinglist:

    The problem is that the user_activation_key field is empty unless a
    reset password request has already been sent for the user. If the link
    in the reset password email is clicked, the key is returned to blank
    when the new password is added.

    The only way to have a non-blank key is to request a password reset and
    not click on the password reset link in the email.

    Thus, if you request http://domain/wp-login.php?action=rp&key%5B%5D=, then
    the key validity check is bypassed, a non-user-specific query is
    executed, and the first user that is pulled up by the query will have
    the password reset.

    Chris Jean

    Hence, the problem is that all users have a blank user_activation_key by default. Querying for such users returns the first one, which is likely the admin ;)

  • Updating WordPress to 2.8.4 (available now via the usual upgrade processes) will fix this problem.

  • Thanks Joe. It only lasted about 24 hours until they released it :-)