小议OAuth 2.0的state参数—从开发角度说《互联网最大规模帐号劫持漏洞即将引爆》

jackxiang 2015-6-24 13:59 | |
背景:正常的授权流程,用户点击授权后便不再可控,剩下的工作由第三方应用和授权服务器(资源提供方)进行交互来完成。而攻击者可以阻止授权流程的正常进行,将中间的关键URL截取下来,诱骗用户访问,成功后可以将受害人的账户绑定到攻击者的微博账户上。此后,攻击者可以使用微博的账户自由登入受害人的主站账户及浏览器账户,任意查看和修改用户的隐私数据。

有一个外国研究员在最近研究OAuth 2.0的登录过程中发现了许多程序员常犯的错误(见翻译文:http://www.freebuf.com/articles/web/5997.html ),原始地址见 http://homakov.blogspot.jp/2012/07/saferweb-most-common-oauth2.html 知道创宇测试国内各网站后发现确为如此,并在今天发表了《互联网最大规模帐号劫持漏洞即将引爆》(http://tech.ccidnet.com/art/32963/20121109/4448657_1.html )。但这篇文章本身的标题实在过于劲爆,同时微博出现了各种误传,结果成了一场个人看来并不必要的恐慌,以及口水战。这篇文章,主要从开发者的角度,来解释这个漏洞的来龙去脉,并以问答的形式给出一些个人建议和其他看法。

通俗的说一下这个漏洞的表现(虽然不全准确):你有一间房子,证明方法是你有一张房契,上面说“你和房子存在有效关系”。某一天,有个中介叫你签续租合同(带复写纸一式三份那种);你签了,结果被告知房子成了别人了。后经调查,续租合同的第二联,其实是一份转让合同。就这样,房契的关系就变了。

你也许懊恼,为什么没有好好检查这一式三份的合同是否一致。同样,在这个漏洞中,开发者就会懊恼,为什么没有好好使用并检查state参数?

但是在说明这个state参数前,有必要了解大部分程序员所写的绑定OAuth账号流程,由于绑定流程很多,这里挑最常见的“用户在第三方网站A上登录后,通过Authorization code方式绑定微博”流程(也是这个漏洞常见的场景流程):
(1)用户甲到第三方网站A登录后,到了绑定页面。此时还没绑定微博。
(2)绑定页面提供一个按钮:“绑定微博”(地址a:http://aaa.com/index.php?m=user_3rd_bind_sina)
(3)用户甲点击地址a,程序生成如下地址b(为方便大家查看,参数部分均未urlencode以【】包含显示):
https://api.weibo.com/oauth2/authorize?client_id=【9999999】&redirect_uri=【http://aaa.comindex.php?m=user_3rd_bind_sina_callback】&response_type=【code】
(4)用户甲浏览器定向到地址b,授权该应用。
(5)授权服务器根据传递的redirect_uri参数,组合认证参数code生成地址c:
http://aaa.comindex.php?m=user_3rd_bind_sina_callback&code=【809ui0asduve】
(6)用户甲浏览器返回到地址c,完成绑定。

咋看起来,好像没啥问题,毕竟code也是不可预测嘛。但是各开发者有没有想过,地址c实质上是和当前登录用户一点关系都没有的,因为地址c只能证明微博用户信息,但无法证明网站A的用户信息。所以漏洞就此产生了——假设有用户乙和丙,同时发起绑定请求,登录到不同的微博账号,然后在第5步后打住,互相交换地址c,会是什么结果?答案就是用户乙绑定了用户丙的微博,用户丙绑定了用户乙的微博了…...攻击者的目标,其实就是要获取地址c,然后诱骗已登录网站A的受害者点击,从而改变了绑定关系。

为应对这种情况,Oauth 2.0引入了state参数。state参数是什么?看看没多长时间前最终定稿的rfc6749(The OAuth 2.0 Authorization Framework, http://tools.ietf.org/html/rfc6749 ):

state: RECOMMENDED. An opaque value used by the client to maintain state between the request and callback.  The authorization server includes this value when redirecting the user-agent back to the client.  The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12.
(推荐。一个由client使用的不透明参数,用于请求阶段和回调阶段之间的状态保持。此参数将在授权服务器重定向用户代理(如浏览器,译者注)回client时包含。该参数理应使用,以防止章节10.12中描述的CSRF。)(PS:不知咋翻译opaque value,只好翻译为“不透明参数”)

这个参数在许多开放平台上也会有提及,比如新浪微博的Oauth2/authorize(http://open.weibo.com/wiki/Oauth2/authorize ):
用于保持请求和回调的状态,在回调时,会在Query Parameter中回传该参数。开发者可以用这个参数验证请求有效性,也可以记录用户请求授权页前的位置。这个参数可用于防止跨站请求伪造(CSRF)攻击。

然而大多数开发者(包括许多官方SDK),会忽略使用这个state参数。所以,大面积的网站(不乏大站)确实存在这种漏洞,这也是知道创宇认为“互联网最大规模帐号劫持即将引爆”的根源。但是在我看来,有点危言耸听,因为利用条件实属有点苛刻:
(1)攻击者必须了解第三方网站可能的绑定特性,否则攻击极可能失败。绝大多数网站都是一个网站账号只能绑定一个OAuth提供方账号(比如微博帐号)。这种特性,导致这个漏洞在绝大多数网站根本无法快速撒网,只能定向劫持未绑定的用户到攻击者OAuth账号上,而攻击一次后,这个账号必须解绑才能用于别的攻击,导致利用难度增大不少。
(2)攻击者还必须了解被定向劫持用户的上网习惯。在这个漏洞中,绝大部分都需要受害者在第三方网站上处于登录状态,否则攻击基本失效。
(3)攻击者还必须了解第三方网站、以及用户在该第三方网站上存在的利益。现在的攻击有许多都是带有利益的,如果是电商类的话,网站本身的金钱利益驱动可能存在,但又需要判断该用户在该网站是否存在高价值,这就增加了额外的工作量;如果只是娱乐类网站,除了言论相关和用户所拥有的网站管理权,我还想不出有什么可以吸引攻击者去定向攻击。

以上各种条件造就了在攻击实施环节更像是那种一对一的淘宝或者QQ钓鱼手段、或者放入针对高价值目标的社工(或APT)一环中。就前者而言,淘宝、QQ甚至各类热门游戏的钓鱼量之大,安全研究者们应该更清楚;就后者而言,实质还有其它更有效地手段。那么,这个漏洞,还能冠以“最大规模帐号劫持”吗?我相信,地下产业者看到后只会轻蔑的笑一下,然后继续埋头干活……


对于开发者而言,要修复这个漏洞,就是必须加入state参数,这个参数既不可预测,又必须可以充分证明client和当前第三方网站的登录认证状态存在关联(如果存在过期时间更好)。见rfc6749 章节10.12:

The binding value used for CSRF protection MUST contain a non-guessable value (as described in Section 10.10), and the user-agent's authenticated state (e.g., session cookie, HTML5 local storage) MUST be kept in a location accessible only to the client and the user-agent (i.e., protected by same-origin policy).
(这个用于防御CSRF的绑定参数(即state参数,译者注)必须包含一个不可预测的数值(如同章节10.10的描述),还有用户代理(如浏览器,译者注)的认证状态必须保存在只允许client和用户代理两者访问的地方(也就是受到同源策略保护))

这听起来好复杂,其实,随机算一个字符串,然后保存在session,回调时检查state参数和session里面的值,就满足要求了。php示例如下:

构造Oauth2/authorize阶段:
$_SESSION['REQ_STATE'] = md5(uniqid(mt_rand(1, 100000), true));  //生成state参数,并存放于session
//$_SESSION['REQ_STATE'] = $_USER['uid']. '_'. $_USER['reg_ip']. '_'. $state;   //(如果还是怕随机不够,算法多考虑一些独特且不可预知的用户信息吧,reg_ip是个不错的选择)

callback回调检查:
$_state_check = !empty($_SESSION['REQ_STATE']) && ($_SESSION['REQ_STATE'] == $_GET['state']) ? true : false;   //callback回调检查



以下是问答环节:

一、普通用户问答环节

问:有微博提出,针对这个漏洞,解决方法是“微博用户立刻检查“我的应用”,暂取消所有应用授权,再根据需要重新授权”,是否正确?
答:就这个漏洞而言,完全错误!该漏洞的结果是第三方网站帐号被关联到攻击者的微博账号上,而不是自己的微博帐号被关联到攻击者的第三方网站账号。在“我的应用”中解除所有应用并不会对攻击者产生任何影响。还记得房子的例子吧?可以类比为:即使自己自杀,也不能导致房子和攻击者解除房契关系。
正确方法请看知道创宇的方案:“定期查看重要网站(比如你经常看优酷的话就检查优酷)的第三方帐号绑定页面,检查是否有陌生的其他帐号绑定到自身账户,如果发现应立即取消绑定或授权。”


问:如果有一个网站的帐号可绑定多个OAuth帐号(或反过来),我是否很容易中招?(即攻击难度是否降低成可以广撒网?)
答:明显是的。然而,从实际接触的业务来看,这种奇葩应用有理由相信存在量不会多,而且做不好绝对不只有这个漏洞,各平台也基本不允许这类应用存在。
真遇到了这类应用怎么办?如果你不是搞营销的,还是别碰这类应用稳妥,赶紧双向取消绑定吧(奇葩应用取消所有绑定,微博“我的应用”也取消)。


问:如何判断自己是否为高价值用户,容易遭受攻击?
答:简单判断方法就是,你在微博上经常晒自己购物的网站来源,还说自己中了优惠券,就有可能了;又或者你成大嘴了,就容易被盯上社工。但是实际上,偷账号的手段更多,这个漏洞对普通用户意义实在不大,该干啥就干啥吧!


二、开发者问答环节

问:这个漏洞究竟严不严重?
答:“安全无小事”。作为合格的开发者,如果你是对用户负责,又是高价值网站,那是确实挺高的。而综合目前的情况,我个人综合评价是低。原因已经在上面讲了。
而且个人觉得开发者过于关注这个漏洞,其实并不见得是好事,因为这个漏洞实质上暴露的是开发者们对OAuth协议(或者说各种第三方接入协议)的各种流程和参数了解得不足——当然这也有平台甚至协议本身的责任在,看看各wiki和sdk就知道了。历史上相关案例也不少,甚至更严重,比如《淘网址sina oauth认证登录漏洞》:WooYun: 淘网址sina oauth认证登录漏洞 。
基于以上原因,个人非常建议开发者对整个OAuth流程部分(包括登录、绑定、解绑等)都通盘检查一次;阅读各种wiki(如果可以读RFC,那就更好了),对OAuth的相关参数要有相当清晰的了解并合理地运用——但这个过程确实很长,我自己也没完全仔细看各种wiki和rfc,囧。平台的话,加强引导(尤其是SDK)还是很重要的,这点绝对要赞淘宝的@放翁_文初 (http://weibo.com/fangweng )。


问:如果设计为“在绑定状态下不允许更换,需要先解绑再绑定”,那是否意味着没有问题?
答:难说,假设攻击者可以CSRF解绑;或者你的绑定回调callback代码实际上没有检查绑定状态,一样可以遭受这个攻击。如果可以确定不存在上述问题,那还好。


问:为什么OAuth 1.0没有这个问题?
答:问这个问题的都是第三方应用的开发老手啊!想必也是和我一样被一路折磨而来,还不知道哪天到头啊!表示泪流满面!(群众:喂喂喂你干嘛这么激动,扔鸡蛋 -_-#)
其实不是没这个问题,而是OAuth 1.0时代,大多数开发者都会将REQUEST TOKEN写到session(或者和当前用户有关的地方中),攻击者获取到的REQUEST TOKEN根本无法跑到受害者的session中,并且ACCESS TOKEN的时候又必须要用REQUEST TOKEN来换,这样一来,无意间完成了client的用户登录状态维持和校验,这就救了大家一命啊!OAuth 2.0时代,没了三次握手,大多数开发者也没有留意到用户状态校验这点,所以就出事了。还是强调那句,state参数除了随机,还必须可以充分证明和当前网站的登录认证状态存在关联。


问:还是不明白state参数所说的“必须可以充分证明client和当前网站的登录认证状态存在关联”,不是随机就成了么?
答:只要state参数不能校验第三方网站的登陆认证状态,这个漏洞就生效。比如:

构造Oauth2/authorize阶段:
$state = md5(uniqid(mt_rand(1, 100000), true));  //生成state参数
set_cache($state, 1, 1800);  //$state作为cachekey存入缓存。

callback回调检查:
If(!empty($_GET['state'])){
    $_state_check = get_cache($_GET['state']) == 1 ? true : false;   //这块缓存并不能证明是当前第三方网站的用户的,所以state并没有防御这个漏洞的效果
}else{
    $_state_check = false;    
}

解决方法还是要想方设法加入用户登录认证信息的校验,不愿想的话,session足矣。



问:如果想在参数state内传递多个参数,怎么办?
答:强烈建议在传递前进行一次base64编码!在这方面我吃过很大的亏,只是使用http_build_query而没有在最后再加一次base64_encode,导致在最后回调阶段的时候,因为php解析器自动帮我urldecode了参数state,导致数据不完整,哭死了!

摘自:http://mp.weixin.qq.com/s?__biz=MjM5NTg2NTU0Ng==&mid=207501892&idx=2&sn=738dfa2703f5b6b651b7feac1fe25ce5&scene=1&key=af154fdc40fed00394fcd25f8845f3c4ad43957405ea07b652c19b35e71716975ebf5150ebe4deaf626008ef6252daca&ascene=1&uin=MzI0MTA5NzU%3D&devicetype=Windows+7&version=61010029&pass_ticket=qY8W3lx558EdeCIh%2BEXxjz%2FJZtNaFlJttyl5WNYXNfE%3D

作者:jackxiang@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:http://jackxiang.com/post/8138/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!


最后编辑: jackxiang 编辑于2015-6-24 14:04
评论列表
发表评论

昵称

网址

电邮

打开HTML 打开UBB 打开表情 隐藏 记住我 [登入] [注册]