Broken authentication leads to total site takeover in combination with read-only SQLi – (Two-Factor <= 0.7.1)

Affected pluginTwo-Factor (Plugin contributors)
Active installs40.000+
Vulnerable version<= 0.7.1
Audited version0.7.1
Fully patched version0.7.2
Recommended remediationUpdate to version 0.7.2 or higher


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:

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 -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 ) {

$user = get_userdata( $wp_auth_id );
if ( ! $user ) {




Proposed patch

The proposed patch is identical to the one described in great detail here.


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


  • The issue was (partially) fixed by hashing the login nonce before inserting it into the database.

Leave a Reply

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