Mata provides several website plugins such as the Like button and Customer Chat. These are hosted at www.facebook.com and designed for use in iFrames. Communication between the host website and Facebook is implemented using postMessage.
The plugin sends messages to its parent window and the SDK on the Facebook side listens for those messages and dispatches them internally. To prevent arbitrary domains from interacting with it, the SDK enforces two checks on received messages: they must originate from Facebook, and they must include the proper callback identifier, a random string.
The Facebook JavaScript SDK registers a cross-window message listener for messages coming from the Facebook iframe. One of the iframe-handling functions injects an SVG directly into the DOM without sanitization, which could lead to XSS if invoked. There are two issues with this, though: 1) we need to send a postmessage, and 2) we need to have the random identifier.
The author of this post seems to know every quirk on Facebook. To solve problem 1, they found a URL that, when the page was visited, would send an iframe with user-controlled data. It's pretty crazy they found this primitive!
The random identifier was generated using Math.random(). This is insufficient for cryptographically random data and leaves a hole. The seed for randomness appears to be unique per page, so we need to leak the randomness somehow. The window.name() also uses Math.random(). If this could be leaked, it could be extracted.
The listener for the call init:post will reinitialize the iframe, generating a new ID. Since the name of a window can be public, it's possible to leak the name and reverse the random number generator to find the seed. From there, it's possible to calculate the callback string to trigger the DOM XSS on the website.
This attack has a few limitations... The XSS occurs on the user's website and NOT Facebook, and it requires lots of framing on websites to be allowed. Because this would be considered low to medium impact, they decided to review the internal use of this plugin to increase the issue's impact.
Most Facebook pages don't allow the framing required for this exploit. So, they decided to find a generic bypass for the framing. On Android and iOS, the XFO header with frame-ancestors set to any domain would place it in the XFO header with ALLOW-FROM. Since this isn't supported by modern browsers, this was a bypass of the iframe protection, but required frame-ancestors to be on the page.
They found an endpoint that would set the frame ancestors to break the iframe protections. However, it had a token that would require a login CSRF for the account. Since this was useless for XSS, a new constraint was added: keep a valid Facebook page inside an iframe with a useful body and ensure it does not refresh after a session change. They noticed that a business endpoint embedded this page on core facebook.com. We have everything we need!
Here's the full exploit chain:
- Victim visits attacker's Facebook App where the attacker opens a Facebook App Webview.
- Attacker creates that would contain sensitive values like an OAuth token in an iframe on their website. The attacker then performs log out and login CSRF into their own account.
- Attacker creates another iframe with the Facebook page with the customer chat plugin with the known attacker token; this is why the login CSRF was required.
- Attacker saves the name of the window for usage later. They force reinitialization of the iframe to get multiple values to defeat randomness. This allows them to calculate the
Math.random() seed.
- Attacker can now send the payload message to the frame from facebook.com and the callback identifier.
- Payload from previous step triggers XSS on Facebook. Now, the script can read the victims OAuth token.
What a crazy set of issues. It requires SOOOO many small primitives in order to exploit and then even more to increase the impact. I appreciate the patience and the gadgets it took to earn the $66K bounty payout.