Affected plugin | Magic Login Pro |
Active installs | Unknown |
Vulnerable version | <= 1.4.1 |
Audited version | 1.4.1 |
Fully patched version | 1.5 |
Recommended remediation | Upgrade 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 contacted | September 07, 2022 |
First Response | September 07, 2022 |
Fully patched at | September 12, 2022 |
Publicly disclosed | April 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