0x00 前言
初进ChaMd5安全团队,应M姐姐之邀写下这篇技术文章,这篇文章主要讲述本人近期代码审计腾讯的第三方登录SDK包时发现的一个漏洞,阅读这篇文章需要读者对OAuth2.0协议的原理和应用特别熟悉,如果读者对此不了解,请参考以下链接:
OAuth2.0协议RFC 文档:
https://tools.ietf.org/html/rfc6749
QQ开放平台的接入说明:
http://wiki.connect.qq.com/oauth2-0%e7%ae%80%e4%bb%8b
这是我之前写的一篇分析OAuth2.0协议安全性的文章:
https://www.anquanke.com/post/id/98392
0x01 攻击模型分析
我在这篇文章里讲的这种攻击方法是针对OAuth协议的一种CSRF攻击,大家都知道,很多网站都有绑定第三方账号的功能,这种攻击手段就是利用了网站的这种功能,为了深入分析漏洞,我先把绑定第三方账号的流程分析一下:
Step 1:某妹子使用账户密码登录XX网站
Step 2:妹子选择绑定QQ账户,在输入凭证信息后,QQ返回如下形式的链接:
http://xx.com/Index.php?Code=XXX
Step 3: 妹子访问上述链接,服务器接收到code参数,进行验证后与当前用户绑定。
为了说明这种攻击方法,我从2012年全国网络与信息安全峰会上知道创宇的一位大佬的PPT里借用一张图:
这种攻击方式看似复杂其实说起来也很简单,主要的问题出现在了上图的第四步中,即攻击者构造链接http://xx.com/Index.php?Code=XXX,其中Code参数绑定的是攻击者的QQ账户,然后发给Victim诱骗点击(CSRF),Victim点击之后,他的账户就与攻击者的QQ账户绑定了,这样攻击者就能通过其QQ登录Victim的账户,实现用户劫持!
当然了,为了防止这种攻击,腾讯在返回的授权登录链接中增加了一个state参数用于防止类似的CSRF攻击。用户在请求使用QQ登录的时候会生成一个与session绑定的不可预测字符串state,并把这个参数发送给授权服务器,授权服务器验证完用户身份后会返回如下形式的链接:
http://xx.com/Index.php?Code=XXX&state=XXX
在接收到此链接时,xx.com会先检测当前session中的state参数和收到的链接中的state参数是否匹配,如果匹配才会进行下一步的操作。这样的话,即使攻击者把自己的授权链接发给Victim,也会因为state验证失败而无法绑定攻击者的账户(攻击者无法预测Victim的session中的state参数,因此就无法构造恶意链接来进行CSRF攻击)。
0x02 漏洞分析
在详细了解了攻击原理之后,我们来剖析一下QQ登录的官方SDK文件。
这里我们废话不多说,直接定位到核心类文件Oauth.class.php
在这个类文件中qq_login方法用于第一步生成state参数并跳转到QQ授权登录接口,可以看到state参数在第36行生成并紧接着调用recorder类的write方法写入到session中,跟进recorder类继续分析:
定位到32行,write方法是把传入的值写到self::$data中,在看到26行,构造函数中self::$data被初始化为一个空数组。
在最后的析构函数中self::$data被存入到session中,这里看上去没有什么大问题,继续向下分析,我们找到Oauth.class.php中的qq_callback方法:
qq_callback方法用于验证state参数并利用传入的code参数获得access_token,我们主要关注state参数的验证阶段,在第57行我们看到有一个if条件判断当前session中的state参数($state)和链接中传入的state参数($_GET[‘state’])是否匹配,如果不匹配则直接调用showError方法die掉:
但是,问题就出现在这里!之前我们已经分析到,state参数在qq_login方法中生成,但是如果Victim没有调用这个方法,self::$data就会被构造函数初始化为一个空数组,再来看一下recorder类中的read方法:
如果self::$data为空则返回null,因此这里存在一个很明显的逻辑缺陷,只要攻击者把恶意链接的state参数留为空,这时$_GET[‘state’]为空值,$state也为空值,if($_GET[‘state’] != $state)为false,这样就可以完美的躲过CSRF检测,嘿嘿!
0x03 后记
这个漏洞从确认到修复只用了三个小时的时间,腾讯SRC的响应速度也是非常惊人。
在最新版的SDK文件中,在验证state时会先判断当前的session中是否保存有state参数,即判断当前用户是否调用过qq_login方法,这样就可以成功的阻止类似的CSRF攻击了: