
OAuth 2.0 机制原理
OAuth 2.0
是目前最流行的授权机制,用来授权第三方应用,获取用户数据
尽管OAuth 2.0
是当前标准,但一些网站仍在使用旧版本1a
。OAuth 2.0
是从头开始编写的,而不是直接从OAuth 1.0
开发的。结果,两者非常不同。目前,术语 “OAuth
” 专指OAuth 2.0
OAuth
就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token
),用来代替密码,供第三方应用使用
( 如图便是一种 OAuth 授权登录 )
简单来说:第三方登录,实质就是OAuth
授权。
用户想要登录A
网站,A
网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要OAuth
授权
OAuth
身份验证的结果大致类似于基于SAML
的单点登录 (SSO
)
比如说用户登录网站A
需要进行身份验证,且允许使用Google
账户登录:
A
网站让用户跳转到Google
Google
要求用户登录,用户登录Google
后,Google
询问用户:A
网站要求获取xxx
权限/
xxx
信息,是否同意?- 用户同意,
Google
就会重定向回A
网站,并发回一个授权码 A
网站使用授权码向Google
请求令牌Google
返回令牌给A
A
网站使用令牌,向Google
请求用户身份
如果说的官方一点,就是:
OAuth
在 "第三方应用" 与 "服务提供商" 之间,设置了一个授权层
"第三方应用" 不能直接登录 "服务提供商" ,只能登录授权层,以此将用户与客户端区分开来
"第三方应用"登录授权层所用的令牌(token
),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。"第三方应用"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"第三方应用"开放用户储存的资料 当用户登录了第三方应用后,会跳转到服务提供商获取一次性用户授权凭据),再跳转回来交给第三方应用,第三方应用的服务器会把授权凭据和服务提供商给它的身份凭据一起交给服务方,这样服务方既可以确定第三方应用得到了用户服务授权,又可以确定第三方应用的身份是可以信任的,最终第三方应用可以顺利的获取到服务商提供的web API
的接口数据
应用场景
现在各大开放平台,如微信开放平台、腾讯开放平台、百度开放平台等大部分的开放平台都是使用的OAuth 2.0
协议作为支撑
- 客户端
App
使用三方登录; - 微信小程序登录授权;
- 多个服务的统一登录认证中心、内部系统之间受保护资源请求
令牌(token)
令牌在OAuth
中验证身份的作用上与使用密码是一致的,但其有以下特点:
- 令牌生命周期较短,到期遍会失效
- 令牌可被数据所有者撤销,且之后基本不会产生相同的令牌
- 令牌对权限的限制更为严格更加细化,比如说:读令牌、写令牌等
以上的有点让OAuth
在身份验证的过程中使用了令牌而不是密码,以上也可以说是OAuth
的优点
但是虽然具备以上优点,令牌依旧必须保密,令牌泄露的后果和密码泄露一样严重
获得令牌的方式
综上所述:OAuth
的核心就是通过设置一个授权层,向第三方应用颁发令牌
OAuth 2.0
规定了四种获得令牌的方式:
- 授权码(
authorization-code
) - 隐藏式(
implicit
)— 危险 - 密码式(
password
) - 客户端凭证(
client credentials
)
其中有两个概念容易弄混:
- 授权码:请求令牌
- 令牌:到用户进行了认证的地方请求用户的数据
授权码authorization-code
授权码(
authorization code
)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌
也就是最开始举例子所采用的方式,该方式是最常用的流程,也是最安全的
该方式适用于有后端的WEB
应用,这样授权码通过前端传送,令牌则存储在后端,所有与资源服务器的通信都在后端完成,前后端分离,避免令牌被泄露
接下来用Burp
靶场中的链接(做了一些改动)为例进行举例(后面会完整打该靶场)
第一步:在登陆时,要登陆的网站
A
会提供一个链接,点击后便会被重定向到B
网站,进行身份认证并决定是否要将数据给A
网站使用。下面是
A
网站跳转B
网站的示意链接https://oauth-xxxxxx.web-security-academy.net/auth?
client_id=xzx6wagk39j6dni18kdl0&
redirect_uri=https://xxxxxx.web-security-academy.net/oauthcallback&
response_type=code&
nonce=879645905&
scope=openid profile email
其中参数的含义为:
response_type
表示要求返回的类型code
表示要求返回授权码client_id
让B
直到是谁向其发送的请求,用于标识请求的发送方xzx6wagk39j6dni18kdl0
redirect_uri
标识B
网站接受或拒绝请求后跳转的网站https://xxxxxx.web-security-academy.net/oauth-callback
scope
表示要求的授权范围openid profile email
Google 平台默认的scope
就是这三个USER_OPENID("openid", "Associate you with your personal info on Google", true), USER_EMAIL("email", "View your email address", true), USER_PROFILE("profile", "View your basic profile info", true),
第二步:用户跳转后,
B
网站会要求用户登录,用户登陆后,B
会询问是否同意给予A
网站授权,用户同意后,B
网站就会跳到redirect_uri
参数指定的网址。跳转时,会传回一个授权码https://xxxxxx.web-security-academy.net/oauth-callback?code=AUTHORIZATION_CODE
第三步:
A
网站拿到B
返回的授权码后,便可以在后端通过授权码向B
网站请求令牌了[
https://oauth-authorization-server.com/token?
client_id=12345&
client_secret=SECRET&
redirect_uri=https://clientapp.com/callback&
grant_type=authorization_code&
code=a1b2c3d4e5f6g7h8&
scope=openid email profile
]client_id
与client_secret
用于让B
确认A
的身份,而且client_secret
是保密的,所以为保证安全,该请求一般只在后端发送grant_type=authorization_code
表示使用的授权方式为授权码
code=a1b2c3d4e5f6g7h8
上一步拿到的授权码
https://client-app.com/callback
令牌颁发后的回调网址
第四步:
B
网站收到请求以后,就会颁发令牌具体做法是向
redirect_uri
指定的网址,发送一段JSON
数据{ "access_token": "z0y9x8w7v6u5", "token_type": "Bearer", "expires_in": 3600, "scope": "openid email profile", … }
其中
access_token
字段就是令牌
隐藏式implicit
不安全
有些Web
应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端(当然不排除有错误配置为该模式的情况)
RFC 6749
就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit
)
第一步:
A
网站提供一个链接,要求用户跳转到B
网站进行登录,并将B
网站的用户数据授权给A
网站使用/authorization?
client_id=12345&
redirect_uri=https://clientapp.com/callback&
response_type=token&
scope=openid profile&
state=ae13d489bd00e3c24
其中重点关注的参数为:
response_type=token
代表要求直接返回令牌
第二步:用户跳转到
B
网站进行身份的认证,登录后若用户同意授予A
网站访问用户数据的权力,则B
网站就会跳转到redirect_uri
所指定的网址,并且把生成的令牌作为URL
参数,传递给A
网站https://clientapp.com/callback#token=ACCESS_TOKEN
其中ACCESS_TOKEN
部分就是返回的令牌此处返回令牌时并没有以查询字符串的形式返回,而是以锚点(
fragment
)的形式返回,主要受历史原因影响,由于OAuth2.0
允许跳转的网站是HTTP
协议,存在攻击的可能,而浏览器跳转时,锚点不会被发到服务器,减少令牌在传输过程中泄露风险[但注意,这种将令牌直接传给前端的方式依旧是非常危险的,避免使用该方式若必须使用,要保证对令牌有效期控制的很短)]
锚点#
:
#
代表了网页中的一个位置,其右面的字符串就是该位置的标识符,比如在支持锚点的网页中,例如输入以下URL
:
https://www.shtwo.top/pwn1#canary
访问时就会将canary
位置滚动到可视区域
锚点的特性
HTTP
请求不包括#
:锚点的作用在于告诉浏览器定位到哪里,与服务端没有关系,所以发出HTTP
请求时不会携带锚点以及后面的字符串也就是说
https://www.shtwo.top/?color=#fff
实际上发出HTTP
请求时报文中请求的URI
实际是?color=
这也就解释了为什么上述令牌要放在
#
后面返回这种过滤方式是在浏览器层面进行的过滤,所以若将
#
URL 编码为%23
就可以成功发送:https://www.shtwo.top/?color=%23fff
默认情况下,搜索引擎会忽略
URL
中#
的部分由于搜索引擎会忽略
#
后面的部分,所以若是希望Ajax
生成的内容被浏览引擎读取,会有麻烦,此时可以使用#!
来代替,Google
会自动将#!
转成查询字符串__escaped_fragment__
的值
密码式password
非常不安全
RFC 6749
也允许用户直接提交用户名和密码,待登录网站直接拿着用户名和密码到认证网站认证身份,申请令牌
第一步:
A
网站直接要求用户提供B
网站的用户名和密码,拿到以后,A
就直接向B
请求令牌/token?
client_id=12345&
grant_type=password&
username=^USERNAME^&
password=^PASSWORD^&
第二步:
B
网站验证身份通过后,直接给出令牌此时不需要跳转,直接把令牌以
JSON
的形式通过HTTP
回应
毫无疑问,非常不安全
凭证式client credentials
该方式一般不用来请求令牌,多用于多用户共享同一令牌时进行令牌的换
第一步:
A
应用在命令行向B
发出请求/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
第二步:
B
验证通过以后,直接返回令牌
使用令牌
A
网站拿到令牌以后,就可以向B
网站的API
请求数据了。
此时,每个发到B
的API
的请求用户信息的请求,都必须通过在HTTP
头部添加Authorization
字段带上令牌
具体格式为:
Authorization: Bearer ACCESS_TOKEN
更新令牌
B
网站颁发令牌的时候,一般会一次性颁发两个令牌:
- 获取数据
access_token
- 刷新获取新的令牌
refresh_token
在申请的access_token
令牌到期前,用户使用refresh token
发出刷新令牌请求,即可更新令牌
/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN
OAuth 2.0 存在的问题
了解了 OAuth 2.0
的原理后,可以发现其可能存在的问题/漏洞,大多数源于其缺乏内置的安全机制,与开发者/使用者是否采取了正确的获取令牌的方式息息相关,那么接下来就通过Burp
官方提供的靶场来说几种存在安全风险的情况
至于如何判断当前是一个OAuth2.0
的认证,方法很多,而且其特征很明显,相信大家通过前面的介绍已经可以区分,此处就不过多介绍了
风险点 1 :使用不合适的方式获取令牌
存在风险的令牌获取方式:
- 隐藏式
implicit
- 密码式
password
且不对传输的access_token
或username/password
加密的话,就会造成中间人攻击
靶场信息:
目标:
客户端应用程序的错误验证使攻击者有可能在不知道密码的情况下登录其他用户的帐户。
要解决实验室问题,请登录Carlos
的帐户。他的电子邮件地址是carlos@carlos-montoya.net
靶机地址:Lab: Authentication bypass via OAuth implicit flow | Web Security Academy
基本信息:进入靶场后,到登陆界面登陆时发现进行的是基于 OAuth 2.0
的身份认证方式
于是打开Burp
进行抓包
通过其请求包中的参数response_type=token
发现采用的获取令牌的方式为:隐藏式
采用该方式说明access_token
令牌会被传回也就意味着可以被获取到,但前提是返回的带有令牌的JSON
信息不被加密
果然其返回的令牌token
为:
于是可以看到当前网站向提供认证的网站发出了请求的报文中带上了这个token
有了这串token
,我们就可以找到刚刚使用该token
的身份认证的报文中,修改其email
达到绕过的目的
将该报文发到Repeater
进行重放,并替换其email
字段
此时得到了一个用于Set-Cookie
的新session
并进行302
的重定向,将其生成对应的URL
复制到浏览器请求
大功告成
风险点 2 :缺乏随机 token 值
此处的token
值得是用于预防CSRF
所使用的token
,并不是前面所说的access_token
,由于当当前网站成功获得了用来访问存在着认证完用户信息的网站时(获取了access_token
),需要使用access_token
这个令牌确认当前的身份,那么这个access_token
的作用就很像CSRF
中经常尝试获取并利用的session cookie
一样,可以代表当前用户的身份,所以若不加以限制,势必会造成 CSRF 攻击
而对于OAuth
中,常用state
参数来传递一个随机的用户防止CSRF
的token
值
/authorization?
client_id=12345&
redirect_uri=https://clientapp.com/callback&
response_type=token&
scope=openid profile&
state=ae13d489bd00e3c24
拿最开始的例子来说,可以明显看到其携带了state
参数,且是一个随机数
所以若是进行了错误的配置,没有启用state
参数,就有可能造成CSRF
攻击
靶机见:
Lab: Forced OAuth profile linking | Web Security Academy
风险点 3:不完善的redirect_uri
限制
通过前面的学习,我们知道在身份服务器验证用户身份(成功或失败)后,需要302
跳转到redirect_uri
处,那么对于重定位的地址,若不进行限制就可以跳转到任意构造好的URL
,通过恶意构造iframe
标签,引导用户点击发起访问,这也可导致最终的令牌被发送到攻击者构造的 URL 处被攻击者获取
所以,客户端应用程序的最佳做法是在向OAuth
服务注册时提供其真实回调URI
的白名单
靶机见:
Lab: OAuth account hijacking via redirect_uri | Web Security Academy
参考文章/深入学习
OAuth 2.0 authentication vulnerabilities | Web Security Academy
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)