Affected plugin | WordFence |
Active installs | 4+ million |
Vulnerable version | <= 7.6.1 |
Audited version | 7.6.1 |
Fully patched version | – |
Recommended remediation | Removal of the plugin |
Description
The plugin stores users’ emergency backup codes and TOTP secrets as plaintext in the database.
An attacker that can obtain one of the seemingly never-ending read-only SQL-Injections will be able to bypass all 2FA checks for all users indefinitely.
Proof of concept
A look at the Controller_TOTP::validate_2fa method reveals that WordFence stores all TOTP secrets and emergency backup codes as plaintext (in binary form).
public function validate_2fa($user, $code, $update = true) {
global $wpdb;
$table = Controller_DB::shared()->secrets;
$record = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `user_id` = %d FOR UPDATE", $user->ID), ARRAY_A);
if (!$record) {
return null;
}
if (preg_match('/^(?:[a-f0-9]{4}\s*){4}$/i', $code)) { //Recovery code
$code = strtolower(preg_replace('/\s/i', '', $code));
$recoveryCodes = str_split(strtolower(bin2hex($record['recovery'])), 16);
$index = array_search($code, $recoveryCodes);
if ($index !== false) {
if ($update) {
unset($recoveryCodes[$index]);
$updatedRecoveryCodes = implode('', $recoveryCodes);
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `recovery` = X%s WHERE `id` = %d", $updatedRecoveryCodes, $record['id']));
}
$wpdb->query('COMMIT');
return true;
}
}
else if (preg_match('/^(?:[0-9]{3}\s*){2}$/i', $code)) { //TOTP code
$code = preg_replace('/\s/i', '', $code);
$secret = bin2hex($record['secret']);
$matches = $this->check_code($secret, $code, floor($record['vtime'] / self::TIME_WINDOW_LENGTH));
if ($matches !== false) {
if ($update) {
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = %d WHERE `id` = %d", $matches, $record['id']));
}
$wpdb->query('COMMIT');
return true;
}
}
$wpdb->query('ROLLBACK');
return false;
}
The highlighted lines 4,11, and 26 prove that both values are stored as plaintext in the database. Otherwise, it would not be possible to compare them to user input without any conversion.
An attacker can use a read-only SQLi to bypass all 2FA checks for all users indefinitely.
SELECT user_id, HEX(recovery), HEX(secret) from wp_wfls_2fa_secrets
Timeline
Vendor contacted | September 08, 2022 |
First Response | September 08, 2022 |
Fully patched at | – |
Publicly disclosed | April 24, 2023 |
Leave a Reply