Friday, April 19, 2013

A story of $9500 bug in Facebook OAuth 2.0


Recently, I have made a talk at the Hack In The Box conference, where I have wrapped up multiple weaknesses in Facebook authorization protocol OAuth 2.0, reported by me during 2012-2013. Many of those bugs led to access token leaking and to taking (restricted) control over Facebook account, but the most interesting OAuth issue resulted in cross-site scripting. By this moment, the XSS bug is mostly patched by Facebook Security team.

Exploitation of this XSS is quite complicated, and though I have put a few pictures on my slides to explain the flow, nothing can be clearer than a well-formatted bug report itself. Here I am publishing my original advisory with a proof-of-concept code, which I had sent to Facebook Security team, describing the full chain of problems and some mitigation proposals as well:




Multiple vulnerabilities in OAuth/XdArbiter allow cross-site scripting
======================================================================


Overview
========

This submission describes an XSS exploit, which relies on several bugs in OAuth and App/Facebook cross-interaction. The PoC was tested under the latest Chrome and IE 8/9 with Flash. A victim should have Flash plugin installed, should have authorized at least one of "Instant Personalisation" apps (e.g, bing), should have been logged into the Facebook and should click the malicious link. After opening the PoC link within a couple of seconds a message box with cookies and fb_dtsg should appear. Next chapter describes exploitation ideas, flaws and mitigation proposals. Mitigations may of course interfere with some existing functionality unknown to me.


Vulnerabilities and exploitation
================================

1. First, a specially crafted page is loaded into the pagetab and then sends a FB_RPC message to parent (Facebook). This is a relatively small high-level weakness/feature in Facebook, which allows any app to be shown into the page tab and to interact with Facebook via RCP messages, even if not authorized.


2. A "show dialog" method is used as an evil RCP call, whose handler makes an ajax call to http://www.facebook.com/connect/uiserver.php. Here another weakness/feature is that:

  2.1 uiserver.php called with method "permissions.request" will
  302-redirect browser to the submitted redirect_uri if the app is
  authorized, while this script is (possibly) supposed to be a safe
  ajax-only endpoint without any redirects. 

 And two critical vulnerabilities:

  2.2 It is possible to impersonate any other app at the time of
  request to uiserver.php, since all important parameters in URL
  are taken right from the FB_RPC message (except redirect_uri,
  which should point to the sender's domain):

   if (!ra.redirect_uri || p(ra.redirect_uri).getDomain().
   toLowerCase() !== p(this.origin).getDomain().
   toLowerCase())
     ra.redirect_uri = this.origin;
   var va = new i().setMethod('GET').setReadOnly(true).
    setURI(o(ua).setQueryData(ra));

  2.3 Response from uiserver.php is then blindly eval()'ed by
  default (after unshielding)

    _handleXHRResponse: function(ka) {
      var la;
     if (this.getOption('suppressEvaluation')) {
        la = {asyncResponse: new h(this, ka)};
     } else {
       var ma = ka.responseText, na = null;
       try {
        var pa = this._unshieldResponseText(ma);
          try {
            var qa = (eval)('(' + pa + ')');

  Mitigations:
 
  2.1 Depending on the uiserver and api logic, 302-redirects should
  be restricted or avoided in ajax endpoints, otherwise it becomes
  difficult to control the data flows
 
  2.2 Important parameters, such as app_id, should be set by code
  inside the facebook.com domain, and not inside the app code
  (all.js). It should not be possible to call uiserver.php with
  app_id other that that loaded into the pagetab.

  2.3 JSON.parse should be used where possible instead of eval


3. Cross-domain redirects are not allowed in ajax calls, so to settle at something interesting after uiserver.php we need the "redirect_uri" to point to facebook.com domain. In order to do this and to bypass the sender origin check, exploit impersonates facebook.com when sending the FB_RPC message via another critical vulnerability. This flaw appears in the way the xd_arbiter.php implements a nonce checking when using flash as a transport: 

 xd_arbiter.php:
  
    ba = h();
    u[ba] = function(ca, da) { // Protection with a nonce, but too late
      ca = decodeURIComponent(ca);
     l.debug('received message %s from %s', ca, da);
     y.onMessage(ca, da);
    };
    t.init(y.channel, 'FB_XDM_CALLBACKS.' + ba);


 WON-TVLCpDP.swf:

  private function externalInit(param1:String, param2:String) : void
  {
     var channel:String = param1;
    var callback:String = param2;
    if(origin_validated)
    {
      return;
    } 
    origin_validated = true;
    log("init(channel " + channel + ", callback " + callback + ")");
    this.onMessageCallback = callback;
   ...

  public function onMessage(param1:String, param2:String) : void
  {
   log("onMessage from " + param2);
   // Not checking the sender
   ExternalInterface.call(
   this.onMessageCallback, encodeURIComponent(param1), 
   param2);
  }

This nonce check in xd_arbiter is performed too late and only prevents from interaction with a fake flash object. At the same time, the legit swf transparently transfers incoming messages into a valid callback without any checks. It is possible inside the app frame to create a sender-xd_arbiter, a proxy-xd_arbiter and a payload-xd_arbiter and eventually to transfer any message to the parent pagetab controller from facebook.com domain:

  sender-xd_arbiter:
   <iframe name="fb_xdm_frame_http2" src="http://facebook.com/connect/xd_arbiter.php?version=11#channel=my_channel&origin=http%3A%2F%2Ffacebook.com&transport=flash"></iframe>

  proxy-xd_arbiter:
   <iframe name="fb_xdm_frame_http" src="http://facebook.com/connect/xd_arbiter.php?version=11#channel=my_channel_http&origin=http%3A%2F%2Ffacebook.com&transport=flash"></iframe>

  payload-xd_arbiter:
   <iframe name="fb_xdm_frame_http3" src="http://facebook.com/connect/xd_arbiter.php?version=11#FB_RPC:{\"method\":\"evilMethod\", \"params\":[{\"var1\":1, \"=&relation=parent&\":0}]}"></iframe>

The payload-xd_arbiter will invoke parent.frames["fb_xdm_frame_http"].proxyMessage(URL_fragment), which in turn will transfer it to the swf inside fb_xdm_frame_http2 via a flash channel, and then the message will be handled by the facebook RPC handler:

  xd_arbiter.php:

   var ca = z ? h(z) : parent.parent;
    try {
   // aa = message = URL_fragment, ba = "http://facebook.com"
   ca.XdArbiter.handleMessage(aa, ba);
   ...

Apart from this quite serious flaw with origin impersonation, another nasty weakness is leveraged here. The URL fragment of payload-xd_arbiter is treated both as a query string and a JSON object: the message formats/handlers logic is possibly little bit messed up at this point, because it allows a message to go through two different parsers.

  Mitigation:

    A nonce checking should be moved (or doubled) from xd_arbiter
    right into the onMessage function of the flash object. It should
    not be possible to spoof the sender's origin.


4. Now we can send RPC messages to the app controller as if our app page will host on facebook.com domain. With this, we can submit any URL within facebook as a redirect_uri for uiserver.php, and it will be eval'ed starting from the 10th byte (after _unshieldResponseText). To trick the uiserver.php script, we only need to impersonate an app whose domain is indeed "facebook.com". Two small security issues will help to achieve this:

  4.1 An app developer can set any domain for his app at App
  Settings page, even facebook.com

  4.2 There are legit apps with their domain set to facebook.com,
  such as JS SDK (id=114545895322903)

 Mitigations:

  4.1 It could be wise enough not to accept facebook domains at the
  App Settings page

  4.2 It would also be better to separate app domains from various
  facebook domains, if possible


5. One problem for the attacker is that uiserver.php does not accept "display" parameter to be "none", and it will 302-redirect the user's browser only if the app had previously been authorized. So, an adversary need to know one app, which was authorized by victim. This issue can be bypassed for most of the users:

  5.1 Facebook OAuth implementation allows lots of dangerous
  facebook ULRs to be in redirect_uri, such as
  http://facebook.com/dialog/oauth?...

  5.2 There is a list of preauthorized apps, used for Instant
  Personalisation (for example, bing.com)

Or at least an attacker may ask a user to authorize some fancy game requiring no permissions. So, to direct user's browser to some controlled facebook URL during the ajax request, the uiserver.php is called in the following way:
http://www.facebook.com/connect/uiserver.php?method=permissions.request&app_id=AUTHORIZED_APP_ID&redirect_uri=REDIRECT_URI&...,

Where AUTHORIZED_APP_ID is, let's say, bing app id, and REDIRECT_URI is:
http://facebook.com/dialog/oauth?client_id=114545895322903&response_type=token%2Csigned_request%2Ccode&display=none
&redirect_uri=http%3A%2F%2Fwww.facebook.com%2F_EVIL_FACEBOOK_URL&...

dialog/oauth will redirect browser to redirect_uri =
http://www.facebook.com/_EVIL_FACEBOOK_URL in any case. Finally, the RPC handler will try to eval the data from http://facebook.com/_EVIL_FACEBOOK_URL

 Mitigation:

   5.1 Only xd_arbiter script URL should be allowed on a
   facebook.com domain as a redirect_uri. Or at least the set of
   possible redirect_uri's within the facebook.com domain must be
   white-listed with "exact match" filtering.


6. Now we need some container for our javascript code, so that it is stored right on the facebook domain, and this could be, for example, a specially crafted picture, retrieved and processed by the safe_image.php script. Two critical vulnerabilities help to successfuly yield execution to the attacker's javascript payload:

  6.1 The script http://external.ak.fbcdn.net/safe_image.php can be
  called from the facebook domain too:
    http://facebook.com/safe_image.php

  6.2 The _unshieldResponseText does not check the payload or at
  least its first 9 bytes before cutting them:

   _unshieldResponseText: function(ka) {
    var la = "for (;;);", ma = la.length;
    ...
    return ka.substring(na + ma);
   }

So, it is possible to construct an image which looks like a valid javascript without the nine-byte prefix. I used gif format for the PoC and was able to bypass the safe_image.php repacking (if encoded correctly, the script gives me back exactly the same image). I can share my encoder and thoughts about payload formats with you by request.

 Mitigation:

   6.1 Domains for static scripts and with external data should be
   clearly separated from the facebook.com domain

   6.2 The _unshieldResponseText function must check the data before
   cutting it.



Best,
Andrey Labunets
isciurus@gmail.com



UPD 23.04.2013: uploaded my Javascript -> GIF encoder

PoC: https://gist.github.com/isciurus/5418746

GIF image: http://fbdkit.netai.net/pagetab/js_payload.gif
GIF encoder: https://gist.github.com/isciurus/5437231

6 comments:

  1. Detailed and informative! Very nice and keep it up!!

    ReplyDelete
  2. Hi to everybody, here everyone is sharing such knowledge, so it’s fastidious to see this site, and I used to visit this blog daily.
    comment pirater un compte facebook

    ReplyDelete