Stephen Sclafani

Hacking Facebook’s Legacy API, Part 1: Making Calls on Behalf of Any User

July 8th, 2014

Summary

A misconfigured endpoint allowed legacy REST API calls to be made on behalf of any Facebook user using only their user ID, which could be obtained from their profile or through the Graph API. Through REST API calls it was possible to view a user’s private messages, view their private notes and drafts, view their primary email address, update their status, post links to their timeline, post as them to their friends’ or public timelines, comment as them, delete their comments, publish a note as them, edit or delete any of their notes, create a photo album for them, upload a photo for them, tag them in a photo, and like and unlike content for them. All of this could be done without any interaction on the part of the user.

An Interesting Request

When starting a pentest I like to browse the target site with Burp open to get a feel for how the site is structured and to see the requests that the site is making. While browsing Facebook’s mobile site touch.facebook.com the following request caught my attention:

bookmarks_get

The request was used to get your bookmarks. The request was interesting for three reasons: it was making an API call rather than a request to a dedicated endpoint for bookmarks; it was being made to a nonstandard API endpoint (i.e. not graph.facebook.com); the call was not Graph API or FQL. Doing a Google search for bookmarks.get turned up nothing. After some guessing I found that the method notes.get could also be called which returned your notes. Through some more searching I found that the endpoint was using Facebook’s deprecated REST API.

The Facebook REST API

The REST API was the predecessor of Facebook’s current Graph API. All of the documentation for the REST API has been removed from Facebook’s website but I was able to piece together some of it from the Wayback Machine. The REST API consists of methods that can be called by both Web applications (websites) and Desktop applications (JavaScript, mobile, and desktop applications). To make a call an application makes a GET or POST request to the REST API endpoint:

POST https://api.facebook.com/restserver.php

method={METHOD}&api_key={API_KEY}&session_key={SESSION_KEY}&...&sig={SIGNATURE}

The request consists of the method being called, the application’s API key, a session key for a user, any parameters specific to the method, and a signature. The signature is a MD5 of all of the parameters and either the application’s secret, which is generated along with the API key when the application is registered with Facebook, or a session secret which is returned with a session key for a user. Web applications sign requests with their application secret. Requests signed with the application secret can make calls on behalf of users and to administrative methods. Desktop applications sign requests with a user’s session secret. Requests signed with a session secret are limited to making calls only for that user. This allows Desktop applications to make calls without exposing their application secret (which would have to be embedded in the application). An application obtains a session key for a user through an OAuth like authentication flow.

Making Calls on Behalf of Any User

From reading the documentation I knew that the actual REST API endpoint was https://api.facebook.com/restserver.php which meant that the https://touch.facebook.com/api/ endpoint had to be acting as a proxy. This raised the question: What Facebook application was it proxying requests as and what permissions did the application have?

So far I had only called read methods. I attempted to call the publishing method users.setStatus:

users_setstatus

Calling this method updated the status on the account that I was logged in to. The update was displayed as being made via the Facebook Mobile application:

facebook_status

This is an internal application used by the Facebook mobile website. Many internal Facebook applications are authorized and granted full permissions for every user. I was able to confirm that this was the case for the Facebook Mobile application by calling the methods friends.getAppUsers and fql.query. Calling friends.getAppUsers showed that the application was authorized for every friend on the account that I was logged in to. Calling fql.query allowed me to make a FQL query on the permissions table to lookup the permissions that the application had been granted.

That I was being authenticated with the REST server as the account that I was logged in to meant that the proxy had to be generating a session key from my session and passing it with each request. This should have limited my ability to make calls only for that account, however, I noticed that in the documentation for many of the methods a session key is optional if the method is being called by a Web application (i.e. the request is being signed with the application’s secret). For these methods a uid parameter can be passed in place of a session key and set to the user ID of any user who has authorized the application and granted it the required permission for the method being called.

Through calling users.setStatus I had been able to find out what Facebook application the proxy was using, but more importantly I had been able to confirm that the proxy would pass any parameters to the REST server that I included in a request. The question now was: Was the proxy signing requests with the Facebook Mobile application secret or my session secret? And if the proxy was using the application secret, would the REST server accept the uid parameter? Including the uid parameter in a request would not stop the proxy from also passing a session key and there was the possibility that the REST server would reject the request if both were passed.

To test it I tried updating the status on a different account than the one I was logged in to by calling users.setStatus with the uid parameter set to user ID of that account. It worked. The status on the account whose user ID I passed was updated. Not only was the proxy signing requests with the application secret, but equally as important, when passed both a session key and the uid parameter the REST server would prioritize the uid.

The documentation for the REST API states that the use of the uid parameter is limited to only those users who have authorized the application and granted it the required permission for the method being called. Since the Facebook Mobile application had been authorized and granted full permissions for every user, it was possible to use the uid parameter to make calls on behalf of any user using any of methods that supported it.

The following methods can be called with the uid parameter:

message.getThreadsInFolder Returns all of a user’s messages.
users.setStatus Updates a user’s status.
links.post Posts a link to a user’s timeline.
stream.publish Publishes a post to a user’s timeline, friend’s timeline, page, group, or event.
stream.addComment Adds a comment to a post as a user.
stream.removeComment Removes a user’s comment from a post.
notes.create Creates a new note for a user.
notes.edit Edits a user’s note.
notes.delete Deletes a user’s note.

This method is only supposed to delete notes that were created by the user through the application. However, in my tests, when called through the proxy it would delete any note.

photos.createAlbum Creates a new photo album for a user.
photos.upload Uploads a photo for a user.
photos.addTag Tags a user in a photo.
stream.addLike Likes content for a user.
stream.removeLike Unlikes content for a user.


Some methods that required a session key would return additional information when called through the proxy:

users.getInfo Returns information on a user.

This method is only supposed to return the information on the user that is viewable to the calling user. However, when called through the proxy it would return the user’s primary email address regardless of the relationship between the user and the calling user.

notes.get Returns the notes for a user.

This method is only supposed to return the notes for the user that are viewable to the calling user. However, when called through the proxy it would return all of the user’s notes, including their drafts.


In addition to the above user methods, the following administrative methods could be called through the proxy on behalf of the Facebook Mobile application:

admin.getAppProperties Gets the property values set for the application.
admin.setAppProperties Sets the property values for the application.
admin.getRestrictionInfo Returns the demographic restrictions for the application.
admin.setRestrictionInfo Sets the demographic restrictions for the application.
admin.getBannedUsers Returns a list of the users who have been banned from the application.
admin.banUsers Bans users from the application.
admin.unbanUsers Unbans users from the application.
auth.revokeAuthorization Revokes a user’s authorization of the application.
auth.revokeExtendedPermission Revokes a extended permission for a user of the application.
notifications.sendEmail Sends an email to a user as the application.


Disclosure

I reported this issue to Facebook on April 23rd. A temporary fix was in place less than three hours after my report. A bounty of $20,000 was awarded by Facebook as part of their Bug Bounty Program.

Timeline

April 23,  4:42pm – Initial report sent
April 23,  5:50pm – Request for clarification from Facebook
April 23,  6:08pm – Clarification sent
April 23,  6:49pm – Acknowledgment of issue by Facebook
April 23,  7:38pm – Notification of temporary fix by Facebook
April 23,  8:39pm – Confirmation of temporary fix sent
April 29, 11:03pm – Notification of permanent fix by Facebook
April 30, 12:58am – Confirmation of permanent fix sent
April 30,  8:35pm – Bounty awarded

Part 2 Preview

If you looked at the REST API Authentication guide and thought that there might be vulnerabilities there, you would have been correct. Both the Web and Desktop authentication flows were vulnerable to CSRF issues that led to full account takeover. These issues were less serious than the API endpoint issue as they required a user to load links while logged in to their account. However, the links could be embedded in a web page or anywhere where images can be embedded. I have embedded one as an image in this blog post. Click to display it. If you’re logged in to Facebook I’d have full access to your account (the issue has been fixed). In an actual attack loading the link would not have required a click.