Learn how 3DS OUTSCALE runs its Bug Bounty program and go behind the scenes of a recent CVE (Common Vulnerabilities and Exposures) publication.
Bug Bounty at 3DS OUTSCALE
3DS OUTSCALE, as an IaaS Cloud provider, is looking for any security-oriented feedback. For this purpose, we opened our Bug Bounty program on the YesWeHack platform in 2016. Security researchers are encouraged to hunt for bugs on our public IaaS platform, especially on the API and the Cockpit web interface.
We want them to find any ways of bypassing segregation measures, or anything impacting their customer experience or the infrastructure itself! To make it possible, we provide security researchers access to a Cloud account that will allow them to test our infrastructure as if they were regular customers. With limited but sufficient quotas, they may spawn private LANs, virtual machines and manage CPUs, RAM, security groups, etc.
Practical Case: Vulnerability in a WordPress Plugin
In 2021, a security researcher (0xspade) submitted a report on our YesWeHack program. This report pointed out an endpoint (a web address at which users can send requests to perform specific actions) on our official website that was not properly escaping input data. The researcher proved that sending some altered data to this endpoint could lead to local code execution. In short, we were facing an XSS vulnerability.
At 3DS OUTSCALE, we use WordPress to power some of our official websites. We had to conduct an investigation to find out which part of the source code was vulnerable, because WordPress works with plugins, which all have their own logic. This investigation led us to apply a hotfix on our servers while waiting for an official patch from the editor. The technical details can be found in the last part of this article.
After a first unsuccessful contact with the plugin editor, we decided to contact ANSSI (the National Cybersecurity Agency of France) as well as WPScan, which is a French CNA (CVE Numbering Authority) specialized in vulnerabilities that can be found within the WordPress ecosystem.
WPScan notably offers a vulnerability scanner as a plugin/CLI, and maintains a database of all vulnerabilities affecting WordPress and its plugins. They were therefore the right contact to help us move forward, acting as our intermediary between the editor and the marketplace during the whole process.
They helped us make the editor realize the severity of the vulnerability and gave them some time to release patches that fixed the problem. We methodically followed the steps and carried out the process successfully.
Ultimately, two CVEs were created regarding this issue. They were recently published under the references CVE-2021-24814 and CVE-2022-0220.
Official advisories are available here:
- https://wpscan.com/vulnerability/94ab34f6-86a9-4e14-bf86-26ff6cb4383e
- https://wpscan.com/vulnerability/a91a01b9-7e36-4280-bc50-f6cff3e66059
Technical Analysis
# Situation
The issue was in the wordpress-gdpr plugin (version 1.9.25). Used for compliance with GDPR requirements in the EU, it asks the user’s consent for the use of cookies.
Each time a page is loaded on a website using this plugin, the latter performs an asynchronous HTTP request (AJAX) to this specific endpoint: /wp-admin/admin-ajax.php. The request is meant to retrieve the user’s choice regarding the use of cookies.
In order for these calls to be made by every visitor, logged in or not, the developers have been using a non-authenticated AJAX call.
The problem was that the function associated with this AJAX call (check_privacy_settings) returned JSON data, but with a wrong Content-Type (text/html) and including non-sanitized input passed during the request.
When the endpoint is typically used making AJAX calls from the client to the backend, there is no problem because the XHR object handles the response and converts it to whatever is suitable (JSON object). But if we create a malicious request (i.e., with malicious HTML & JavaScript user input) that makes a browser go to this endpoint as if it was a normal browsing page, then the browser is unable to interpret this as JSON data, because of the lack of application/json Content-Type. Consequently, it will display an HTML page with the malicious user input interpreted.
# Exploration
First, how does this plugin work?
When a visitor arrives on a website using this plugin, an AJAX call is made to retrieve some information:
Figure 1 – AJAX call by the vulnerable plugin
Two parameters are sent along with the request, which is supposed to trigger a check_privacy_settings function. This function is actually fired up by a wp_ajax_nopriv_ hook. For those who are not familiar with the WordPress universe, it is a custom AJAX endpoint created by the plugin’s developers and reachable without authentication.
If we look at the function’s code, without going into details, we basically have something like this:
Figure 2 – simplified check_privacy_settings function’s code
We can see that a part of the input (the $setting
variable corresponding to the values of the $settings
associative array) is reinjected into the function’s output, without sanitization.
Let’s play with it.
# Disillusion
The scenario on which we will base our thinking is the following:
- An attacker creates a malicious webpage (which has a safe-looking URL, or at least not suspicious) and sends the link to the victim;
- The malicious page redirects the victim’s browser to the vulnerable endpoint by passing HTML input as POST parameters;
- Once redirected to the vulnerable endpoint, the victim’s browser executes the payload.
Figure 3 – Try #1: The malicious page
In this example, we simply create a form with the same parameters as in the above screenshot, and we automatically submit it via JavaScript. An HTML payload is included in the value of one of the parameters. When browsing this page, we see a Loading… text, then we are automatically redirected to hxxps://vuln-wp.xyz/wp-admin/admin-ajax.php and we get the following content:
Figure 4 – Try #1: The vulnerable endpoint
As you can see, the <h1>
tag is interpreted by the browser because the Content-Type of the response is set to text/html (this is the default Content-Type returned by admin-ajax.php).
But we also have some escaped characters. That is because of the json_encode
function which escapes the data to make it JSON valid. This is why "
, '
, /
, and \
are being escaped.
What we have to understand is that it won’t be possible to execute code with <script></script>
because the closing tag is mandatory for the JavaScript code to be interpreted. Knowing that, how can we produce a working exploit?
# Motivation
As we want the exploit to be transparent to the victim, we are looking for a way to automatically execute JavaScript code inside a page, without any user interaction needed (click or anything else).
We know that the use of <script>
tags is not possible. Another option would be to use HTML events. HTML elements can trigger JavaScript code under some circumstances (e.g. element is clicked, mouse is moved over, image is loaded, window is resized, etc.). The large majority of these events require an interaction between the user and the webpage. Except for the onload event, which is fired up once an element is loaded (image, body, etc.).
The onload event works well with <img>
and <body>
tags. Moreover, <img>
is a void element and HTML does not require to close it, and the <body>
‘s closing tag is also not mandatory for the browser to interpret it.
We are now able to execute JavaScript code automatically. We can submit a value without the use of quotes to an HTML attribute and, in this case, we must do so:
Figure 5 – Try #2: The malicious page
And the resulted behavior:
Figure 6 – Try #2: The vulnerable endpoint
However, we are somewhat limited in the code we can run due to escaped characters, as seen earlier. Thus we need to find workarounds.
# Annihilation
We are forced to remove all whitespaces in the JavaScript payload because the browser will cut the script if it encounters a whitespace, thinking that it acts as a separator between the onload value and the next attribute for this HTML element (this is because we didn’t surround the onload content with quotes).
Furthermore, if we want to use variables inside the payload, we have to declare them without the var
keyword. This technique is valid and won’t trigger a ReferenceError as long as we don’t use strict mode (and we won’t).
For return statements, we may use return(value)
instead of return[space]value
.
And remember, we can’t use single and double quotes! Meaning we won’t be able to use standard strings in the script we write for the onload event. We have two options to work around this problem:
- Using backticks (
`
) instead of quotes given that template literals can be used in the same way as strings; - Using an array of charcodes without spaces (
[104,101,108,108,111]
) and converting it into a string with a function such as the following:
// Function.prototype.apply() : String.fromCharCode.apply(null, charcodes) // OR Spread syntax : String.fromCharCode(...charcodes)
Thanks to these little tweaks, we are able to make the victim’s browser execute whatever we want with, for instance, something like eval(atob(`base64encodedPayload`))
.
Now, imagine the victim is an administrator of the targeted WordPress, with a valid session cookie (i.e., they are already logged in as admin on the site). Because the admin panel is on the same domain as this vulnerable endpoint, AJAX calls are possible and not restricted (i.e., we don’t need explicit CORS permissions), and iframe manipulation is not restricted by the same-origin policy. We may then perform any action we want on the admin panel on behalf of the administrator (user creation, article edition, plugin installation, etc.), and even upload a webshell to keep full access to the WordPress in the future and try lateral movements or privilege escalation.
# Resolution
The easiest, and fastest, way to protect against this vulnerability is to adapt the Content-Type each time a function returns JSON data.
The plugin’s editor first decided to add a nonce check for this request in their 1.9.26 patch. Unfortunately, this wasn’t enough since it didn’t fix the XSS vulnerability but only slightly reduced the severity. In fact, a nonce has a default validity of 24 hours (twice its default 12 hours lifetime). However, if it’s compromised, the admin may still be exposed to an attack during this time. But above all, the problem is that the nonce is the same for all unauthenticated visitors. All four variables used to create the nonce ($uid
, $token
, $action
, $i
) have the same value if the user is not logged in. This means that the endpoint is still vulnerable to phishing issues. One could redirect an unauthenticated victim to this page and execute JavaScript code. For example, it would be possible to create a fake Login page to steal the admin’s credentials. The only constraint for the attacker is to update the nonce in their malicious code once a day.
Eventually the editor added a casting to integer on to the input data (the famous $setting
variable, since the client is supposed to send a number) in the version 1.9.27. Now a string payload is converted to int(0) and there is no more XSS. As there are one partial patch and one complete patch, two distinct CVEs have been opened.
If you are a cybersecurity researcher yourself and would like to discover the Cloud, you are welcome to join our bug bounty program! You just have to create a YesWeHack account and contact us so that we can provide you with free access to the platform. Hope to see you soon 😉