Friday, December 16, 2011

Doing Cross Page Communication Correctly


I haven't updated this blog in more than one year (woops), but it seems like I still have a couple of followers, so I was thinking on what to write about. I was originally planning to post this on August, but the fix was delayed more than expected.

I decided to choose a random target on the interwebs to find an interesting vuln, and since Facebook recently launched it's "Whitehat Program", which rewards people that report them security vulnerabilities (kinda the same as Google's Vulnerability Reward Program), I chose them.

(Note: As of  December 15, Facebook says they have fixed the vulnerability, and awarded a $2,500 USD bounty).

So, I took a look at their "main JS file": http://connect.facebook.net/en_US/all.js

And well, first thing that came to my mind was RPC. Mostly, because I worked implementing the Apache Shindig's version of the Flash RPC, and have helped reviewing easyXDM's implementation, I just knew this is too hard to get right.

A simple grep for ".swf" in their all.js file lead us to "/swf/XdComm.swf". And since I didn't know what domain that was on I tried:

https://www.facebook.com/swf/XdComm.swf

And that worked.

So let's see.. I sent it to showmycode.com and we get this:
http://www.showmycode.com/?e97c7f39457f9e1b1d5b2167e14b968e

There are several non-security-bugs in that code (some of which I decided to ignore for brevity and keep the WTF quota of this blog low).

In general the security problems found are not specific to FB at all, they are mostly, side effects of bad design decisions from either Flash or the browsers. However, this problems are widely known and can be abused by attackers to compromise information.

Calling security.allowDomain

The first thing I notice is that XdComm calls Security.allowDomain and Security.allowInsecureDomain. This allows to execute code in the context of https://www.facebook.com/ so it's an Flash-XSS, FAIL #1.

The way you exploit this is by loading the victim SWF inside the attacker's SWF. That's it. The problem here is that Adobe provides only one API for enabling two very different functionalities. In this case, what Facebook wants is just allow an HTML container to call whitelisted 'callbacks' from the SWF, but inadvertently it is also allowing anyone to load the SWF inside another SWF and access all methods and variables, which can result in code execution.

Adobe actually acknowledges this is a problem, and they will make changes to support this two different use cases. The reason I don't provide a PoC is because there are several applications out there that depend on this behavior and can't easily deploy any fixes, and Adobe is working on fixing this at Flash (which is where it should be fixed). When there's a viable alternative or a good solution I'll post a PoC.
What FB should have done is keep this SWF out of www.facebook.com.
Getting the embedding page location


The second thing I notice is that it's getting the origin of the page hosting the SWF calling:
this.currentDomain = ExternalInterface.call("self.document.domain.toString");
And as any Flash developer should know, ExternalInterface.call isn't something you can actually trust, so now you can "cheat" XdComm.swf into thinking it's being embedded by a page it isn't by simply overriding __flash__toXML.

So, by abusing this vulnerable check, we can actually, listen and send messages on any LocalConnection channel. This doesn't only mean we just defeated the security of the transport, but that also, if any other SWF file uses LocalConnection in facebook.com (or fbcdn.net), we can sniff into that as well. So, FAIL #2.

It is hard, for a movie (or a plugin whatsoever) to know with certainty where it's being hosted. A SWF can be sure it's being hosted same domain, by requiring the hosting page to call a method in the Movie (added by ExternalInterface.addCallback), since by default, Flash only allows movies hosted in the same domain to call callback methods of a movie (this is what we do in Shindig for example), but besides that it's not so simple.

Some insecure methods exist and are widely used to know the hosting page, such as calling:
ExternalInterface.call("window.location.toString")
There are some variations of that code, such as calling window.location.href.toString, which is also simple to bypass by rewriting the String.toString method, and works on all browsers.

It's futile to try to "protect" those scripts, because of the way Flash handles ExternalInterface, it's possible to modify every single call made by the plugin, since when you call ExternalInterface.call, what really happens is that the plugin injects a script to the window with:
ExecScript('try { __flash__toXML(' + yourCode + ') ; } catch (e) { "<undefined;>"; }');

And, __flash__toXML is a global function injected by Flash, which can be modified to return whatever we want.
(function(){
    var o;
    window.__flash__toXML = function () { return o("potato") };
    window.__defineSetter__("__flash__toXML", function(x) {o = x;});
})();
It's worth noting that Flash also bases some of it's security decisions on the value of window.location (such as, if a movie is allowed to be scripted from a website or not), and while this check is more difficult to tamper (and browsers actively fix it), it's still possible to do it, and it's even easier on other browsers such as Safari (in Mac OS) where you can just replace the function "__flash_getWindowLocation" and "__flash_getTopLocation".

Luckily, it seems like we might be able to get at least the right Origin in future versions of Flash, as Mozilla is proposing a new NPAPI call just for this. Let's just hope that Adobe makes this available to the SWF application via some API.

What FB should have done is namespace the channel names, and use some other way of verifying the page embedding the SWF (like easyXDM or Shindig does).

It is also possible for an attacker to specify what transport it wishes to use, so we might be able to force a page to use the Flash transport even when it might also support postMessage.

postMessage should be used cautiously

There's one last thing I found. Facebook has a file which seems to allow an attacker to forge (postMessage) messages as coming from https://www.facebook.com/ into another page that allows framing arbitrary pages.

The Proof of Concept is located at http://r.i.elhacker.net/fbpwn

As you can see the page will allow an attacker to send messages and will also allow the attacker to specify the target origin. The attack seems to be hard to do since the "parent" seems to be hard coded. So this is FAIL #3.

This is a good demonstration why the existing implementation of postMessage is fundamentally broken, it's really easy for two different scripts to interfere with each other. I can't actually blame FB for that, it's more like a design problem in postMessage.

Luckily there's a new mechanism to use postMessage (called channel messaging), which partly solves this problem (or at least makes it harder to happen). You can read more about it here:

Random fact.. This is what Chrome uses internally to communicate with other components like the Web Inspector.

Vendor Response

I reported these issues from https://www.facebook.com/whitehat on Tuesday Aug 16 2011 at 2 PM (PST), with the draft of this blogpost, and got a human acknowledgement at 7PM. The issue was finally fixed on December 15 2011.

Conclusion

So well, this was my first post of 2011 (it's December!), and I actually made it because there was a few "de facto" knowledge about Flash that I wanted to put in writing somewhere, and because I had a look at Facebook regarding something not strictly related to work!

In general I am impressed on the security of Facebook applications. While doing this I got locked out of my account like 5 or 6 times (maybe they detected strange behavior?), I noticed several security protections in their API (api.facebook.com/graph.facebook.com), and they actually do protect against other security vulnerabilities that most websites don't know about (such as ExternalInterface.call escaping bugs, content type sniffing, etc).

I was awarded a $2,500.00 USD bounty for this report (not sure how it was calculated), and I'm considering donating it to charity (it can become 5k!). Any suggestions?