AWS SSO 中的新 PKCE 认证带来希望(大部分情况下)
2021年,我曾撰文介绍攻击者如何利用 AWS SSO 设备码进行钓鱼攻击,使现代安全控制(如 FIDO 认证或身份提供商的设备状态检查)失效:通过 AWS SSO 设备码认证进行 AWS 凭据钓鱼。在本文中,我们将更详细地探讨 AWS SSO 认证流程中新发布的 PKCE 支持。
设备码钓鱼的简史
- Sebastian Mora 撰写了一篇博客文章并发布了“awsssome_phish”,这是一个旨在操作设备码钓鱼攻击的攻击性工具。
- Chaim Sanders 推送了一篇文章,讨论了 AWS 生态系统中设备码钓鱼的相关风险。
- Matthew Garrett 指出,设备码认证有效地绕过了近年来实施的许多高级安全机制。
- Rami McCarthy 发布了一份很好的意见指南,概述了 AWS 应采取的潜在步骤来解决此问题。
- Chris Norman 发布了一个开源浏览器扩展,可以保护用户免受设备码钓鱼攻击。
虽然我没有看到关于实际利用的报告,但我直接收到了至少两个红队的反馈,称设备码钓鱼非常有效。
re:Invent 2024——新希望
在 re:Invent 前几天,AWS 宣布发布“基于 PKCE 的 SSO 授权”,该功能现已可用,并在 AWS CLI 版本 2.22.0 及更高版本中默认启用。虽然 PKCE(Proof Key for Code Exchange)这个术语可能听起来晦涩,但它本质上是标准 OAuth 2.0 授权码流程的一种实现。
基于 PKCE 的认证流程的新 UI
以下是概念上描述该流程的序列图:
该流程与设备码认证的根本区别在于,成功认证后,AWS 会将用户重定向到 localhost。在这里,客户端应用程序(本例中为 AWS CLI)通过启动一个临时 Web 服务器来检索授权码。关键含义是,创建恶意认证链接的攻击者无法检索授权令牌来冒充受害者。即使受害者通过 AWS SSO 成功认证,他们也会被重定向到 localhost 并遇到浏览器错误,从而使攻击无效。
这些钓鱼网站从来不起作用,太疯狂了!
实现细节如何工作?
AWS CLI 的实现应该能让你有个很好的了解(更具体地说是 SSOTokenFetcherAuth 类)。我还复制了一个简单且丑陋的 Python 实现如下。
示例 Python 实现(点击展开)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
"""Note: you need a recent boto3 version. Make sure to replace your SSO start URL below too."""
SSO_START_URL='https://d-1234.awsapps.com/start'
import boto3
import jwt
import json
import urllib.parse
import secrets
import base64
import hashlib
def generate_code_verifier_and_challenge():
# Generate a cryptographically random code_verifier
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode('utf-8')
# Compute the code_challenge from the code_verifier
digest = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('utf-8')
return code_verifier, code_challenge
sso_oidc = client = boto3.client('sso-oidc')
redirect_uri = "http://127.0.0.1"
response = client.register_client(
clientName='pkce-demo',
clientType='PUBLIC',
scopes=["sso:account:access"],
redirectUris=[
redirect_uri
],
grantTypes=["authorization_code", "refresh_token"],
)
client_id = response['clientId']
client_secret = response['clientSecret']
code_verifier, code_challenge = generate_code_verifier_and_challenge()
url = "https://oidc.us-east-1.amazonaws.com/authorize?response_type=code"
url += f"&client_id={client_id}"
url += f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
url += "&state=dontcare"
url += "&code_challenge_method=S256"
url += "&scopes=sso%3Aaccount%3Aaccess"
url += f"&code_challenge={code_challenge}"
# Decode the JWT in client_secret without validating it
claims = jwt.decode(client_secret, options={"verify_signature": False})
# Read token interactively
print("Please open this URL in your browser:")
print(url)
print()
print("When you get redirected to localhost, paste the OAuth code below:")
code = input("OAuth code: ")
response = client.create_token(
grantType='authorization_code',
clientId=client_id,
clientSecret=client_secret,
redirectUri=redirect_uri,
codeVerifier=code_verifier,
code=code
)
access_token = response['accessToken']
print("Successfully retrieved an AWS SSO access token.")
print("Available AWS accounts:")
sso_client = boto3.client('sso')
response = sso_client.list_accounts(accessToken=access_token)
for account in response.get('accountList', []):
print(f" - {account['accountName']} ({account['accountId']})")
"""
Now generate some creds!
response = sso_client.get_role_credentials(
roleName='account-admin',
accountId='111111',
accessToken=access_token
)
print(response)
"""
|
示例输出:
1
2
3
4
5
6
7
8
9
|
Please open this URL in your browser:
https://oidc.us-east-1.amazonaws.com/authorize?response_type=code&client_id=icBsllQ8y8kJWfyMjKH3cnVzLWVhc3QtMQ&redirect_uri=http%3A//127.0.0.1&state=dontcare&code_challenge_method=S256&scopes=sso%3Aaccount%3Aaccess&code_challenge=wJYlfWmU3ejXGVbUbiXhahQHUaexSrlcLSQahygGCkk
When you get redirected to localhost, paste the OAuth code below:
OAuth code: eyJr...TB2d5pEmNPNJ6GMNypXkAr
Successfully retrieved an AWS SSO access token.
Available AWS accounts:
- Sandbox (12345678901)
- Honeypots (1111111111)
|
攻击者能否使用恶意重定向 URL 窃取授权令牌?
OAuth 实现中最常见的漏洞之一是允许攻击者制作重定向到攻击者控制域的恶意认证 URL。成功时,这允许攻击者拦截授权令牌并冒充受害者。
在这种情况下,AWS 通过将重定向 URL 限制为 localhost 来减轻此风险。此限制确保由 AWS CLI 启动的临时 Web 服务器是授权码的唯一接收者。我尝试使用各种技术绕过此验证,包括嵌入 IPv4 组件的 IPv6 地址和 DNS 重绑定。然而,所有尝试均未成功——这是一个好迹象,表明实现是健壮的(或者我是个糟糕的渗透测试员)。
这真的不能用于钓鱼目的吗?
绝对不行——至少设计上不行,不像设备码认证那样天生容易受到钓鱼攻击。唯一潜在的风险是如果 AWS 的实现存在漏洞,例如攻击者能够绕过重定向 URL 允许列表。如果你是一名熟练的应用安全人员,我鼓励你尝试一下:这将是一个很好的提交到 AWS 在 HackerOne 上的新 VDP 程序!
那么……我们安全了吗?
不幸的是,尽管基于 PKCE 的认证是正确的方向,但设备码认证仍然可用,并且无法禁用它。因此,此新版本并未关闭它本应减轻的攻击向量。
AWS 以其对向后兼容性的坚定承诺而闻名,可能提前发布此功能以推动采用并逐步扩展,并可能计划允许组织在未来强制执行基于 PKCE 的认证。
你应该做什么?
- 将你的 AWS CLI 升级到最新版本(2.22.0+),该版本默认使用基于 PKCE 的流程(注意:aws-vault 中的支持即将到来)。
- 鼓励组织中的每个人过渡到基于 PKCE 的流程。
- 使用 CloudTrail 检测和警报旧设备码流程的使用情况。你可以通过将 CloudTrail 日志与以下属性匹配来创建警报:
- eventName = CreateToken
- eventSource = sso.amazonaws.com
- requestParameters.grantType = urn:ietf:params:oauth:grant-type:device_code
如果你有权访问 EDR 或代理,你还可以在网络级别阻止 device.sso..amazonaws.com。这将确保没有人可以从公司设备使用设备码认证。
如果设备码钓鱼对你来说是一个问题,请告知你的 AWS TAM,以便他们可以在内部倡导。
感谢阅读!你可以在 LinkedIn 和 Bluesky 上找到我。
参考文献