Site takeover by stealing login tokens – (Magic Login Pro < 1.4.1)

Affected pluginMagic Login Pro
Active installsUnknown
Vulnerable version<= 1.4.1
Audited version1.4.1
Fully patched version1.5
Recommended remediationUpgrade to version 1.5 or higher

Description


The plugin stores login tokens as plain text in the “wp_usermeta” table, which is equally as dangerous as storing passwords in plaintext since anybody with access to a login token can authenticate himself as the target user.

Proof of concept


The function that creates login tokens looks like so:

function create_user_token( $user ) {
	$settings  = get_settings(); // phpcs:ignore
	$tokens    = get_user_meta( $user->ID, TOKEN_USER_META, true );
	$tokens    = is_string( $tokens ) ? array( $tokens ) : $tokens;
	$new_token = sha1( wp_generate_password() );

	$ip = sha1( get_client_ip() );
	if ( defined( 'WP_CLI' ) && WP_CLI ) {
		$ip = 'cli';
	}

	$tokens[] = [
		'token'   => $new_token,
		'time'    => time(),
		'ip_hash' => $ip,
	];

	update_user_meta( $user->ID, TOKEN_USER_META, $tokens );

	if ( absint( $settings['token_ttl'] ) > 0 ) { // eternal token
		wp_schedule_single_event( time() + ( $settings['token_ttl'] * MINUTE_IN_SECONDS ), CRON_HOOK_NAME, array( $user->ID ) );
	}

	return $new_token;
}

As an aside, using sha1 to hash the output of wp_generate_password does not make sense since wp_generate_password is already sufficiently random.

First, the plugin generates a random token (line 5.) and adds it to the already existing token array in the “wp_usermeta” table. The token is then sent to the user’s email address.

Anybody that can access a token is able to log in as the user that requested it by visiting:

/wp-login.php?magic_login=1&user=TARGET_USER_ID&token=STOLEN_TOKEN

The plugin compares the user-provided token against all plaintext tokens stored in the database for TARGET_USER_ID.

$tokens        = get_user_tokens( $user->ID, true );
$is_valid      = false;
$current_token = null;
foreach ( $tokens as $i => $token_data ) {
	if ( hash_equals( $token_data['token'], $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$is_valid      = true;
		$current_token = $token_data;
		unset( $tokens[ $i ] );
		break;
	}
}

This means that an attacker can log in as any user that has recently requested a login token IF he has obtained any of the seemingly never-ending read-only SQL-Injections in any plugin, theme, or WordPress Core.

SELECT meta_value FROM wp_usermeta WHERE meta_key = 'magic_login_token'

Proposed patch


Login tokens are password equivalent and MUST be hashed before being inserted into the database.

Then, once a user tries to log in the user-provided token must be hashed and compared against the stored hashes. This way, nobody with access to just the hashes can use them to log in.

A sample implementation might look like this:

// Creation.

$random_token = bin2hex(random_bytes(32));

// Send $random_token to the user

$store_me = hash_hmac('sha256', $random_token, wp_salt('auth'));


// Validation. 

$store_me = /* GET TOKEN FROM DATABASE */
$user_provided_token = /* GET TOKEN FROM REQUEST */

$valid = hash_equals($store_me, hash_hmac('sha256', $user_provided_token, wp_salt('auth');

Timeline


Vendor contactedSeptember 07, 2022
First ResponseSeptember 07, 2022
Fully patched atSeptember 12, 2022
Publicly disclosedApril 24, 2023

Miscellaneous


  • The vendor has shown exceptional cooperation and implemented our proposed patches within less than four business days.
  • The vendor did not hide the vulnerability in his release notes and urged all users to update immediately.

Leave a Reply

Your email address will not be published. Required fields are marked *