2FA bypass by deleting a hidden input field – (WP 2FA <= 2.2.0)

| in


Affected pluginWP 2FA
Active installs30.000+
Vulnerable version<= 2.2.0
Audited version2.0.0
Fully patched version2.2.1
Recommended remediationUpgrade to version 2.2.1 or higher

Description


An attacker can bypass all two-factor authentication checks by deleting a hidden input field in the 2FA form.

Proof of concept


The Login::login_form_validate_2fa method contains a logical flaw
which allows an attacker to completely bypass the check of a user’s configured two-factor authentication provider.

The plugin supports the following 2FA methods, each of which is assigned an identifier in the code:

  • Emergency code sent to the user’s email (Key: “email”)
  • TOTP (Key: “totp”)
  • Backup codes that the user printed out (Key: “backup_codes”)

The trimmed-down logic of the method is as follows:

public function login_form_validate_2fa()
{
  // EDITOR: nonce is verified here.
  
  if (isset($_POST['provider'])) {                                       // phpcs:ignore
    $provider = sanitize_textarea_field(wp_unslash($_POST['provider'])); // phpcs:ignore
  }
  
  // Validate TOTP.
  if ('totp' === $provider && true !== self::validate_totp_authentication($user)) {
    // EDITOR: TOTP is validated here.
    // EDITOR: This branch calls exit() on validation failure only.
  }
  
  // Backup Codes.
  if ('backup_codes' === $provider && true !== self::validate_backup_codes($user)) {
    // EDITOR: backup codes are validated here.
    // EDITOR: This branch calls exit() on validation failure only.
  }
  
  // Validate Email.
  if ('email' === $provider && true !== self::validate_email_authentication($user)) {
    // EDITOR: backup codes are validated here.
    // EDITOR: This branch calls exit() on validation failure only.
  }
  
  // EDITOR: The user is logged in here
}

The critical part is here:

if (isset($_POST['provider'])) {                                       
    $provider = sanitize_textarea_field(wp_unslash($_POST['provider'])); 
  }

The “$provider” variable is only assigned if the post request
contains a “provider” key. Otherwise, it is treated as NULL by PHP.

Thus, an attacker can skip all checks (since $provider is always NULL) and he will be logged in directly.
By merely removing the “provider” hidden input field (or setting it to an arbitrary value) in the browser’s dev tools the entire plugin can be bypassed.
Of course, this also works by sending a curl request that does not specify a provider key.

Proposed patch


There needs to be much better input validation in this method.
An example fix could look like this:

$valid_providers = [
    'email',
    'totp',
    'backup_codes',
];

if (
    ! isset($_POST['provider'])
    || ! is_string($_POST['provider'])
    || ! in_array($_POST['provider'], $valid_providers, true )) {
  
  wp_die('Forbidden', 'Forbidden', 403);
  
}

Timeline


Vendor contactedThe issue was already fixed before we could submit the report.
First Response
Fully patched atMay 02, 2022
Publicly disclosedApril 24, 2023

Miscellaneous


  • The vendor did not disclose this critical vulnerability whatsoever in the release notes of the plugin. We think it was either fixed by accident or withheld.

Leave a Reply

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