Friday, March 8, 2013

Hacking Github with Webkit

Previously on Github: XSS, CSRF (My github followers are real, I gained followers using CSRF on bitbucket), access bypass, mass assignments (2 Issues Reported forever), JSONP leaking, open redirect.....

TL;DR: Github is vulnerable to cookie tossing. We can fixate _csrf_token value using a Webkit bug and then execute any authorized requests.

Github Pages

Plain HTML pages can served from yourhandle.github.com. These HTML pages may contain Javascript code.
Wait.
Custom JS on your subdomains is a bad idea:
  1. If you have document.domain='site.com' anywhere on the main domain, for example xd_receiver, then you can be easily XSSed from a subdomain
  2. Surprise, Javascript code can set cookies for the whole *.site.com zone, including the main website.

Webkit & cookies order

Our browsers send cookies this way:

Cookie:_gh_sess=ORIGINAL; _gh_sess=HACKED;

Please have in mind that Original _gh_sess and Dropped _gh_sess are two completely different cookies! They only share same name.
Also there is no way to figure out which one is Domain=github.com and which is Domain=.github.com.
Rack (a common interface for ruby web applications) uses the first one:

cookies.each { |k,v| hash[k] = Array === v ? v.first : v }

Here's another thing, Webkit  (Chrome, Safari, and the new guy, Opera) sends cookies ordering them not by Domain (Domain=github.com must go first), and even not by httpOnly (they should go first obviously).
It orders them by the creation time (I might be wrong here, but this is how it looks like).

First of all let's have a look at the HACKED cookie.

PROTIP — save it as decoder.rb and decode sessions faster:


ruby decoder.rb
BAh7BzoPc2Vzc2lvbl9pZCIlNWE3OGE0ZmEzZDgwOGJhNDE3ZTljZjI5ZjI1NTg4NGQ6EF9jc3JmX3Rva2VuSSIxU1QvNzR6Z0h1c3Y2Zkx3MlJ1L29rRGxtc2J5OEd3RVpHaHptMFdQM0JTND0GOgZFRg%3D%3D--06e816c13b95428ddaad5eb4315c44f76d39b33b

{:session_id=>"5a78a4fa3d808ba417e9cf29f255884d", :_csrf_token=>"ST/74zgHusv6fLw2Ru/okDlmsby8GwEZGhzm0WP3BS4="}
  1. on a subdomain we create _gh_sess=HACKED; Domain=.github.com
  2. window.open('https://github.com'). Browser sends: Cookie:_gh_sess=ORIGINAL; _gh_sess=HACKED;
  3. Server responds: Set-Cookie:_gh_sess=ORIGINAL; httponly ....
  4. This made our HACKED cookie older then freshly received ORIGINAL cookie. Repeat request: 
  5. window.open('https://github.com'). Browser sends:
    Cookie: _gh_sess=HACKED; _gh_sess=ORIGINAL;
  6. Server response: Set-Cookie:_gh_sess=HACKED; httponly ....
  7. Voila, we fixated it in Domain=github.com httponly cookie. Now both Domain=.github.com and Domain=github.com cookies have the same HACKED value.
  8. destroy the Dropped cookie, the mission is accomplished:   document.cookie='_gh_sess=; Domain=.github.com;expires=Thu, 01 Jan 1970 00:00:01 GMT';
Initially I was able to break login (500 error for every attempt). I had some fun on twitter. Github staff banned my repo. Then I figured out how to fixate "session_id" and "_csrf_token" (they never get refreshed if already present)

It will make you a guest user (logged out) but after logging in values will remain the same.

Steps:

  1. let's choose our target. We discussed XSS-privileges problem on twitter a few days ago. Any XSS on github can do anything: e.g. open source or delete a private repo. This is bad and Pagebox technique or Domain-splitting would fix this.

    We don't need XSS now since we fixated the CSRF token.
    (CSRF attack is almost as serious as XSS. Main profit of XSS - it can read responses. CSRF is write-only).

  2. So we would like to open source github/github, thus we need a guy who can technically do this. His name is the Githubber.
  3. I send an email to the Githubber.
    "Hey, check out new HTML5 puzzle! http://blabla.github.com/html5_game"
  4. the Githubber opens the game and it executes the following javascript — replaces his _gh_sess with HACKED (session fixation):

  5. HACKED session is user_id-less (guest session). It simply contains session_id and _csrf_token, no certain user is specified there.
    So the Game asks him explictely: please Star us on github (or smth like this) <link>.
    He may feel confused (a little bit) to be logged out. Anyway, he logs in again.
  6. user_id in session belongs to the Githubber, but _csrf_token is still ours!
  7. Meanwhile, the Evil game inserts <script src=/done.js> every 1 second.
    It contains done(false) by default — it means, keep submitting the form to iframe :

    <form target=irf action="https://github.com/github/github/opensource" method="post">
    <input name="authenticity_token" value="ST/74zgHusv6fLw2Ru/okDlmsby8GwEZGhzm0WP3BS4="
    </form>
  8. At the same time every 1 second I execute on my machine:
    git clone git://github.com/github/github.git 
  9. As soon as the repo is opensourced my clone request will be accepted. Then I change /done.js: "done(true)". This will make Evil game to submit similar form and make github/github private again:

    <form target=irf action="https://github.com/github/github/privatesource" method="post">
    <input name="authenticity_token" value="ST/74zgHusv6fLw2Ru/okDlmsby8GwEZGhzm0WP3BS4="
    </form>
  10. the Githubber replies: "Nice game" and doesn't notice anything (github/github was open sourced for a few seconds and I cloned it). Oh, his CSRF token is still ST/74zgHusv6fLw2Ru/okDlmsby8GwEZGhzm0WP3BS4=

    Forever
    . (only cookies reset will update it)



Fast fix — now github expires Domain=.github.com cookie, if 2 _gh_sess cookies were sent on https://github.com/*.
It kills HACKED just before it becomes older than ORIGINAL.

Proper fix would be using githubpages.com or another separate domain. Blogger uses blogger.com as dashboard and blogspot.com for blogs.

Last time I promised to publish an OAuth security insight

This time I promise to write Webkit (in)security tips in a few weeks. There are some WontFix issues I don't like (related to privacy).

P.S.
I reported the fixation issue privately only because I'm a good guy and was in a good mood.
Responsible disclosure is way more profitable with other websites, when I get a bounty and can afford at least a beer.
Perhaps, tumblr has a similar issue. I didn't bother to check

24 comments:

  1. Nice. One tactic recommended by OWASP to at least try to mitigate session fixation is to switch session-ids and CSRF tokens on successful login, and only set the user-id in the newly created session. Would that be effective here?

    ReplyDelete
    Replies
    1. > only set the user-id in the newly created session
      how can you know if it was newly created?

      yes, changing csrf token and session_id would be helpful, but there are other cookies and it is tedious ..

      Delete
    2. By "newly created session", I meant the one with the ID that just got assigned (so before the login event, it didn't exist).

      Delete
    3. this is really hard to track. app level session and user session are different things. If github would store user_id in active record session I would be able to hijack the whole account.

      Delete
  2. what's the difference between
    domain .github.com and domain github.com?


    Thanks,

    ReplyDelete
    Replies
    1. .github.com means *.github.com - Cookies is sent for any domain ending with github.com

      Delete
  3. So, will you publish the repo?

    I just read on hackernews your answer to this

    ReplyDelete
  4. You could use the PATH attribute to set it to the specific github path request and all browsers will send your HACKED cookie first.

    ReplyDelete
    Replies
    1. And the fix from Github can still be bypassed by Cookie Stacking (Knock out the original cookie and replace it with HACKED cookie)

      Delete
    2. yes, I forgot that PATH is more specific and i would need only 1 request. You are right.

      Github fixes it by refreshing csrf token after login - you cant hack main website now

      Delete
  5. Why don't Rails signed cookies protect against this? "If you change this key [Application.config.secret_token], all old signed cookies will become invalid." Why is the hacked cookie, which is not signed with secret_token, accepted by Rails at all? Shouldn't Rails be rejecting this cookie outright? Thank you.

    ReplyDelete
    Replies
    1. hacked cookie is legit signed cookie which i simply copy from my current browser session.

      Delete
    2. I see. Could this be fixed by creating a separate CSRF token for every session id?

      Delete
    3. it could be fixed by refreshing csrf token after login, because it will remove fixated value

      Delete
  6. Can you elaborate a little on the fragment bellow please?....i'm really trying hard to understand this.

    so if on website www.github.com , there is javascript like

    document.domain='github.com' .....what can happen?

    "If you have document.domain='site.com' anywhere on the main domain, for example xd_receiver, then you can be easily XSSed from a subdomain".

    ReplyDelete
    Replies
    1. i open x=window.open(path with such code) and do document.domain='site.com' on my subdomain. Both windows have same origin - site.com - and now subdomain can xss main domain

      Delete
  7. Could this attack be mitigated by using ensuring that the origin of the request URL matches the origin of the request referrer?

    Eg. This request was made to the origin github.com, but originated on page.github.com so discard it.

    ReplyDelete
    Replies
    1. there is no way to know origin. csrf token is technique to do this

      Delete
    2. Sorry, perhaps I'm misunderstanding the attack.

      In the demo, you make a HTTP AJAX request to github.com/protected/action from pages.github.com.

      My understanding is that browsers include a "Referer" header when making an HTTP request - in this case, the AJAX request to the protected resource would have the Referer header value "pages.github.com".

      GitHub could test whether origin of the URL of the protected resource matches the origin of the Referer header. In this case github.com != pages.github.com, so they could reject the request.

      Delete
    3. never rely on referrer,
      1 https doesnt send it
      2 it's not AJAX, its just form

      Delete
  8. Can we fix this problem by signing all the cookies from the main domain? When server code receives the cookies string, it may just ignore all entries with incorrect signatures.

    ReplyDelete
    Replies
    1. All rails sessions are already signed. Thing is - i drop MY signed cookie. It's still signed but i know what csrf token is inside. And I 'fixate' it.

      Delete