Penetration testing & window.opener — XSS vectors part 2

tldr; opener.location.* and the onhashchange event are XSS vectors. XSS exists in old versions of reveal.js.

This is the second part of a four part series exploring security concepts related to popup windows and the JavaScript opener variable. This post assumes that you have read the first part available here. In the first part we came to the conclusion that the only issue that exists in relation to the opener variable is a poor-mans client-side open redirect. The open redirect came about because a malicious website can control the location variable of a cross domain window.

opener.location DOM XSS

Our goal is to abuse our control over the location variable to trick client-side JavaScript into executing JavaScript commands that we control. An obvious and well known way that the location value can be abused is when an application injects it’s value into the DOM in an unsafe way. For example, the following JavaScript snippet creates an a tag (hyperlink) derived from the window’s current location.

document.body.innerHTML = "<a href='" + window.location + "/nextPage'>unsafe!</a>"

A specially crafted URL could achieve XSS, for example http://xss.vg/'onClick='alert(1) would be rendered in the DOM tree with the following value:

<a href="http://xss.vg/" onClick="alert(1)" nextpage="">unsafe!</a>

Notice how the href attribute has become http://xss.vg/ and the onClick JavaScript handler has been injected as an additional attribute to the tag (If this makes no sense to you I suggest you go and read up about XSS Basics and DOM based XSS). What if we change our vulnerable JavaScript snippet to use the opener instead? Is it still vulnerable?

document.body.innerHTML = "<a href='" + opener.location + "/nextPage'>unsafe!</a>"

When we try our malicious URL against the snippet above we get an error about the opener variable not being defined:

To get around this issue we need to make sure the opener variable is defined before JavaScript attempts to make use of it . The obvious way to populate the variable is to use the open function to load the malicious URL right? But how do we run the open function without already having the ability to run JavaScript on the vulnerable page…? What if we trick the victim into visiting a website we control and we call the open function from our website? That won’t work either because when the vulnerable website attempts to read the location value of our malicious website (as the malicious site is the opener) it will be violating the same origin policy and the browser will block it.

There are two ways that I can think of to solve our same origin policy problem. The simplest way is to set the opener to be our own window by using the _self keyword when we open our malicious link. The _self keyword will cause the URL to be loaded in the current window with the added benefit of populating the opener variable for us such that opener === window. When the vulnerable JavaScript encounters the opener.location command it will be equivalent to window.location and we get our XSS.

open("http://xss.vg/'onClick='alert(1)", "_self")

This strategy works for Chrome and Firefox but in Edge, opening with _self doesn’t populate the opener variable through a page load. To make this work in Edge we need to get a little tricky (confusing).

Our goal isn’t to bypass the same origin policy exactly, this would be a zero day in the browser’s security model. Instead we want to setup the windows so that they will satisfy the same origin policy. We will do this by navigating the opener to the same domain as the vulnerable page before loading the vulnerable page. Make sense? maybe this animation will help:

onhashchange event XSS

One element of a URL that is sometimes used in client side JavaScript is the location.hash value. If we attempt to set the hash value of a cross domain windows, the browser security controls step in and block us.

We can set the hash value indirectly if we include it in the full URL like so:

opener.location = "http://xss.vg/#HashValueHere";

One interesting thing about the hash segment is that changing it doesn’t trigger a page reload when it is the only part of the URL that changes.

location = location + "#first" //doesn't trigger a page reload, will focus the element with the id 'first' if it exists and trigger an onhashchange event
location = location + "/oops" //causes the page to reload as the pathname has changed
location = location + "?oops" //causes the page to reload as the search (query string) has changed

What happens when we do the same thing to the opener’s location?

opener.location = opener.location + "#first" //error, we aren't allowed to read the value of opener.location
opener.location = "http://vulnsite/" //this is ok
opener.location = "http://vulnsite/#hash change" //triggers onhashchange event on the opener's window!

Some websites monitor the value of the hash to make logic decisions in the application. For example, Gmail uses the hash value when performing an email search (among other functionality). In the GIF below you can see the URL’s hash changing and the search input box is being updated with the same value. The hash is being changed by JavaScript running cross domain:

Don’t panic, being able to cause searches to happen in Gmail cross domain doesn’t mean that an attacker is able to do anything nefarious but I imagine this would freak some people out. To be clear, this is not XSS. Although JavaScript in one domain as causing another domain to do something, we aren’t able to execute arbitrary JavaScript commands in the mail.google.com domain. Below is a simple script that will search your Gmail with this cross domain hash changing magic:

index = 0; 
setInterval(function() {
var hash = 'you+have+been+hacked++++++';
var url = "https://mail.google.com/mail/u/0/#search/" + hash.substr(0, index++ % hash.length);
open(url,'gmailPopup')
},1000);

The open(url, ‘gmailPopup') function call in the snippet above has a second parameter with the value 'gmailPopup'. This parameter is used to give the popup window a name . By including this parameter, the browser first looks to see if a window with the name gmailPopup exists and if it does will set the location value to the value of url (the first parameter) rather than creating a new popup window. This is important because if a new popup window was created every time you called the open function you will simply be opening lots of new windows with different hash values as opposed to changing the hash value of an existing window.

Why is it important that we change the hash value rather than load windows with a specific hash value? So that we can trigger the onhashchange event in the window with a hash value that we control! Consider the following JavaScript snippet that uses hash change events to focus different elements on the page (using JQuery):

location.hash = "";
$(window).on('hashchange', function() {
$(location.hash.split('#')[1]).focus();
});

With the code above it wouldn’t be enough to change the location to http://vulnerable/#<img src=x onerrer=alert() /> as the vulnerable code is only triggered on a hash change (not a page load). To trigger a vulnerability such as this you could do something like this:

var payload = "#<img src=x onerror=alert()/>";
open(vulnerablePage, "popup")
//give the vulnerable page a few seconds to fully load before changing the hash value
setTimeout(function() { open(vulnerablePage + payload, "popup");
}, 2000);

This is getting more interesting as even security conscious developers may miss this issue as it isn’t obvious that a cross domain site could trigger a hash change event.

Example of unsafe opener usage — Reveal.js

At this stage you might be thinking to yourself “does this sort of code really exist”. Well, I thought the same thing and using a little Google-fu found that older versions of reveal.js have a DOM based XSS issue through it’s use of the opener.location variable in the presenter notes functionality (versions prior to v2.6.0). The vulnerable line of code is shown below:

https://github.com/hakimel/reveal.js/blob/4164200474e2af27803dc7683054f5443743c8a9/plugin/notes/notes.html#L142

document.write( '<iframe width="1280" height="1024" id="current-slide" src="'+ window.opener.location.href +'"></iframe>' );

Injecting XSS into this snippet is a little difficult as we need to break out of the src attribute’s double quotes ("). The problem is that double quotes are URL Encoded in all browsers as %22. The same thing goes for < and > So a URL like:

http://xss.vg/"></iframe><script>alert()</script>

Would become this in the resulting HTML:

<iframe width="1280" height="1024" id="current-slide" src="http://xss.vg/%22%3e%3c/iframe%3e%3cscript%3ealert()%%3c/script3e"></iframe>

Which is safe from XSS. However, at the time I discovered this issue, the hash value in Chrome and Edge were not URL encoded so the following URL:

http://xss.vg/#"></iframe><script>alert()</script>

Would become:

<iframe width="1280" height="1024" id="current-slide" src="http://xss.vg/#"></iframe><script>alert()</script>"</iframe>

And the alert() XSS would fire. At the time of writing, Chrome had changed its behavior and the hash value is also URL encoded so this issue is only present in MS Edge/Internet Explorer.

The story so far

So far we have discussed pop-up windows and the opener variable. We have seen that browsers have implemented security controls that disallow JavaScript execution to occur between windows based on the same-origin policy checks but the opener.location variable can be used as an XSS vector and can trigger HashChange events. Join me in part three (coming soon) as we play around with cross-window eval.

Josh is a senior penetration tester at TSS specialising in web application penetration testing.
TSS is a specialist cyber security company providing penetration testing, security assurance consulting and managed security services. More information is available at our website https://www.tsscyber.com.au.