【THM】JWT Security
JWT Security
------了解 JWT 是什么,它们被用在何处,以及它们需要如何进行安全防护。
简介
在这个房间中,你将学习 JSON Web Tokens (JWTs) 及其相关的安全性。随着 API 的兴起,基于令牌的身份验证变得越来越流行,而 JWTs 仍然是其中最受欢迎的实现之一。然而,使用 JWTs 时,确保安全实现至关重要。不安全的实现可能导致严重的漏洞,攻击者可以伪造令牌并劫持用户会话!
前置条件
学习目标
- 学习基于令牌的认证
- 学习 JSON Web 令牌(JWT)
- 学习 JWT 为什么受欢迎
- 学习使用 JWT 时的安全注意事项
- 学习如何攻击易受攻击的 JWT 实现
基于令牌的认证
API 的兴起
应用程序编程接口,简称 API,如今已变得非常流行。这种繁荣的关键原因之一是能够创建一个单一的 API,然后同时为多个不同的接口服务,例如 Web 应用程序和移动应用程序。这使得相同的服务器端逻辑可以集中并重用于所有接口。从安全角度来看,这也通常是有益的,因为它意味着我们可以在一个 API 中实现服务器端安全,然后无论使用何种接口,该 API 都可以保护我们的服务器。
然而,随着 API 的兴起,也出现了新的会话管理方法。由于 Cookie 通常与通过浏览器使用的 Web 应用程序相关联,因此基于 Cookie 的 API 身份验证通常效果不佳,因为解决方案对其他接口不是通用的。这就是基于令牌的会话管理登场救场的地方。
基于令牌的会话管理
基于令牌的会话管理是一个相对较新的概念。它不使用浏览器的自动 cookie 管理功能,而是依赖客户端代码来处理。在身份验证后,Web 应用程序会在请求体中提供一个令牌。然后,使用客户端 JavaScript 代码,将此令牌存储在浏览器的 LocalStorage 中。
当发起新的请求时,JavaScript 代码必须从存储中加载令牌并将其作为头部附加。最常见的令牌类型之一是 JSON Web Tokens (JWT),它通过 Authorization: Bearer 头部传递。然而,由于我们没有使用浏览器的内置 cookie 管理功能,这有点像西部荒野,任何事都可以发生。尽管有标准,但没有强制要求必须遵守这些标准。JWT 这样的令牌是标准化基于令牌的会话管理的一种方式。
API 项目
在这个房间中,你将对多个 API 进行利用。API 可以使用多种不同的方法进行文档化。一种流行的方法是创建一个 Postman 项目或 Swagger 文件。虽然我们鼓励你尝试这些解决方案,但它们需要你有一个账户,而我们在本房间中避免强制要求。相反,我们提供了 API 的简化解释,如下所示。除了最后一个例子外,所有例子的 API 保持一致,最后一个例子具有附加功能。在完成练习时,请参考本节进行指导。该 API 是用 Python Flask 开发的。因此,代码示例将是 Python 语言。
API 端点
API 项目有一个单一的 API 端点,即http://MACHINE_IP/api/v1.0/exampleX。其中的 X 被替换为示例的编号。此端点访问两种 HTTP 方法:
- POST:为了进行身份验证并接收您的 JWT,您需要以 JSON 格式发送包含凭证的 POST 请求。
- GET:为了获取有关您的用户的信息,并最终执行权限提升以恢复您的任务标志。
API 凭证
要向 API 进行身份验证,需要以如下方式发送包含凭证的 JSON 正文:
- username: user
- password: passwordX
X 需要替换为示例的编号。
API 示例
以下是您可以使用与 API 交互的两个 cURL 请求。用于身份验证,可以执行以下 cURL 请求:
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://MACHINE_IP/api/v1.0/exampleX
用于用户验证,可以执行以下 cURL 请求:
curl -H 'Authorization: Bearer [JWT token]' http://MACHINE_IP/api/v1.0/example2?username=Y
[JWT token] 组件需要被替换为从第一个请求中收到的 JWT。在这种情况下, Y 可以是 user 或 admin ,具体取决于您的权限。
PS:举例如下
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MSwiYXVkIjoiYXBwQiJ9.jrTcVTGY9VIo-a-tYq_hvRTfnB4dMi_7j98Xvm-xb6o' http://MACHINE_IP/api/v1.0/example7_appA?username=admin
API 权限
每个示例的主要目标都是获取管理员权限并验证这些权限。一旦你拥有一个有效的 JWT,其中 admin 设置为 1,你就可以请求管理员用户的详细信息。这将返回你的 flag。第一个示例的过程将展示出来,但你需要将步骤复制到其余的示例中。
启动机器
现在你已经了解了 API 结构,差不多该开始实践了。点击右下角的“启动机器”按钮来启动这台机器。你可以使用 AttackBox 或你的 VPN 连接来访问虚拟机。在机器启动的同时,让我们来探索 JWT 的工作原理。

JSON Web Tokens
JWT 是一种自包含的令牌,可用于安全地传输会话信息。它是一个开放标准,为任何希望使用 JWT 的开发者或库创建者提供信息。JWT 的结构如下面的动画所示:
JWT 结构
一个 JWT 由三个部分组成,每个部分都经过 Base64Url 编码,并由点分隔:
- 头部 - 头部通常指示令牌的类型,即 JWT,以及所使用的签名算法。
- 负载 - 负载是令牌的主体,其中包含声明。声明是提供给特定实体的信息。在 JWT 中,有预定义声明,这些声明由 JWT 标准预先定义,以及公共声明或私有声明。公共声明和私有声明是由开发者定义的。了解公共声明和私有声明的区别是有意义的,但出于安全目的并非如此,因此这不会是我们在此讨论的重点。
- 签名 - 签名是令牌中提供验证令牌真实性的部分。签名是通过使用 JWT 头部中指定的算法创建的。让我们深入了解一下主要的签名算法。
签名算法
JWT 标准中定义了多种不同的算法,但我们真正关心的只有三种主要算法:
- None - None 算法表示未使用任何算法进行签名。实际上,这意味着一个没有签名的 JWT,因此无法通过签名来验证 JWT 中提供的声明。
- 对称签名 - 对称签名算法(如 HS256)通过在生成哈希值之前,将密钥值附加到 JWT 的头部和正文中来创建签名。任何知道密钥的系统都可以执行签名验证。
- 非对称签名 - 非对称签名算法(如 RS256)通过使用私钥对 JWT 的头部和正文进行签名来创建签名。这是通过生成哈希值,然后使用私钥对哈希值进行加密来创建的。任何知道与用于创建签名的私钥相关联的公钥的系统都可以执行签名验证。
签名安全
JWT 可以加密(称为 JWE),但 JWT 的核心功能来自签名。一旦 JWT 被签名,就可以发送给客户端,客户端可以在任何需要的地方使用这个 JWT。我们可以有一个中央认证服务器来创建用于多个应用程序的 JWT。然后每个应用程序都可以验证 JWT 的签名;如果验证通过,JWT 中提供的声明就可以被信任并执行。

敏感信息泄露
敏感信息泄露
我们将首先探讨的一个常见问题是 JWT 中敏感信息的泄露。
一种常见的基于 Cookie 的会话管理方法是使用服务器端会话来存储多个参数。例如,在 PHP 中,您可以使用 $SESSION['var']=data 来存储与用户会话关联的值。这些值不会在客户端暴露,因此只能从服务器端恢复。然而,对于令牌来说,声明会作为整个 JWT 发送到客户端。如果遵循相同的发展实践,敏感信息可能会被泄露。在真实应用程序中可以看到一些例子:
- 使用密码哈希进行凭证泄露,甚至更糟糕的是,明文密码作为声明被发送。
- 暴露内部网络信息,例如认证服务器的私有 IP 或主机名。
实际示例 1
让我们来看一个实际示例。让我们使用以下 cURL 请求来验证我们的 API:
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password1" }' http://MACHINE_IP/api/v1.0/example1
这将为您提供 JWT 令牌。一旦恢复,解码 JWT 的正文以揭示敏感信息。您可以手动解码正文,或使用 JWT.io 等网站进行此过程。
开发错误
在示例中,敏感信息被添加到了声明中,如下所示:
payload = {
"username" : username,
"password" : password,
"admin" : 0,
"flag" : "[redacted]"
}
access_token = jwt.encode(payload, self.secret, algorithm="HS256")
修复
密码或 flag 等值不应作为声明添加,因为 JWT 将在客户端发送。相反,这些值应在后端服务器上安全存储。当需要时,可以从经过验证的 JWT 中读取用户名,并用于查找这些值,如下面的示例所示:
payload = jwt.decode(token, self.secret, algorithms="HS256")
username = payload['username']
flag = self.db_lookup(username, "flag")

签名验证错误
JWT 的第二个常见错误是未能正确验证签名。如果签名未被正确验证,攻击者可能会伪造一个有效的 JWT 令牌来访问另一个用户的账户。让我们来分析常见的签名验证问题。
未验证签名
签名验证的第一个问题是当没有进行签名验证时。如果服务器没有验证 JWT 的签名,那么就有可能修改 JWT 中的声明为你想要它们成为的任何内容。虽然不太常见会找到没有进行签名验证的 API,但签名验证可能被遗漏在 API 的单个端点中。根据端点的敏感性,这可能对业务产生重大影响。
实践示例 2
让我们认证到 API:
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password2" }' http://MACHINE_IP/api/v1.0/example2
一旦通过身份验证,让我们验证我们的用户:
curl -H 'Authorization: Bearer [JWT Token]' http://MACHINE_IP/api/v1.0/example2?username=user
然而,让我们尝试在不验证签名的情况下验证用户,移除 JWT 的第三部分(只留下点),然后再次发起请求。你会发现验证仍然有效!这意味着签名没有被验证。将 payload 中的 admin 声明修改为1,然后尝试以管理员用户身份验证以获取你的 flag。
开发错误
在示例中,签名没有被验证,如下所示:
payload = jwt.decode(token, options={'verify_signature': False})
虽然正常 API 中很少看到这种情况,但在服务器到服务器的 API 中经常发生。如果威胁行为者可以直接访问后端服务器,JWT 可以被伪造。
修复
JWT 应始终进行验证,或在服务器间通信时使用额外的身份验证因素,例如证书。JWT 可以通过提供密钥(或公钥)进行验证,如下面的示例所示:
payload = jwt.decode(token, self.secret, algorithms="HS256")
降级为 None
另一个常见问题是签名算法降级。JWT 支持名为 None 的签名算法,这实际上意味着在 JWT 中不使用任何签名。虽然这听起来有点荒谬,但该标准背后的想法是服务器之间的通信,其中 JWT 的签名在一个上游过程中进行验证。因此,第二个服务器将不需要验证签名。然而,如果开发人员没有锁定签名算法,或者至少拒绝使用 None 算法,那么你可以简单地更改 JWT 中指定的算法为 None,这将导致签名验证库始终返回 true,从而再次允许你伪造令牌中的任何声明。
实际示例 3
向 API 进行身份验证以获取您的 JWT,然后验证您的用户。为了执行此攻击,您需要手动修改标头中的 alg 声明为 None。您可以使用 CyberChef 来完成此操作,利用 URL 编码的 Base64 选项。再次提交 JWT 以验证它是否仍然被接受,即使签名不再有效,因为已经进行了修改。然后,您可以修改 admin 声明以恢复标志。
开发错误
虽然这看起来与之前的问题相同,但从开发角度来看,它稍微复杂一些。有时,开发者希望确保他们的实现支持多种 JWT 签名验证算法。然后,实现通常会读取 JWT 的头部,并将找到的 alg 解析到签名验证组件中,如下所示:
header = jwt.get_unverified_header(token)
signature_algorithm = header['alg']
payload = jwt.decode(token, self.secret, algorithms=signature_algorithm)
作为算法,签名验证被绕过了。 然而,当威胁行为者指定 NonePyjwt,本房间使用的 JWT 库,已经实现了安全编码以防止此问题。如果选择 None 算法时指定了密钥,则会抛出异常。
修复
如果需要支持多种签名算法,应将支持的算法作为数组列表提供给解码函数,如下所示:
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])
username = payload['username']
flag = self.db_lookup(username, "flag")
弱对称密钥
如果使用对称签名算法,JWT 的安全性依赖于所使用的密钥的强度和熵 。如果使用弱密钥,可能可以进行离线破解以恢复密钥。一旦知道密钥值,您可以再次修改您的 JWT 中的声明,并使用密钥重新计算一个有效的签名。
实用示例 4
在这个示例中,使用了一个弱密码来生成 JWT。一旦你收到一个 JWT,你就有几种方法来破解密码。在我们的示例中,我们将讨论使用 Hashcat 来破解 JWT 的密码。你也可以使用其他解决方案,例如 John。你可以按照以下步骤来破解密码:
- 将 JWT 保存到一个名为 jwt.txt 的文本文件中。
- 下载一个常见的 JWT 密码列表。在这个房间中,你可以使用
wget https://raw.githubusercontent.com/wallarm/jwt-secrets/master/jwt.secrets.list来下载这样的列表。 - 使用 Hashcat 来破解密钥,使用
hashcat -m 16500 -a 0 jwt.txt jwt.secrets.list
一旦你知道了密钥,你就可以伪造一个新的管理员令牌来恢复旗帜!
开发错误
这个问题发生在使用弱 JWT 密钥时。这通常发生在开发人员赶时间或从示例中复制代码时。
修复
应选择一个安全的密钥值。由于此值将用于软件而不是人类,因此应使用一个长且随机的字符串作为密钥。
签名算法混淆
签名验证的最后一个常见问题是当可以进行算法混淆攻击时。这与 None 降级攻击类似,但它特指对称和异步签名算法之间的混淆。如果使用异步签名算法,例如 RS256,可能会将算法降级为 HS256。在这些情况下,某些库可能会默认将公钥用作对称签名算法的秘密。由于公钥是已知的,你可以结合使用 HS256 算法和公钥来伪造一个有效的签名。
实践示例 5
这与示例 3 类似。但这次不允许使用 None 算法。然而,一旦你认证到示例,你也会收到公钥。由于公钥不被视为敏感信息,因此经常会找到公钥。有时,公钥甚至作为 JWT 声明嵌入其中。在这个示例中,你必须将算法降级为 HS256,然后使用公钥作为秘密来签名 JWT。你可以使用以下脚本来帮助你伪造这个 JWT:
import jwt
public_key = "ADD_KEY_HERE"
payload = {
'username' : 'user',
'admin' : 0
}
access_token = jwt.encode(payload, public_key, algorithm="HS256")
print (access_token)
注意:我们建议您使用 AttackBox 来运行这个实际示例,因为 Pyjwt 已经为您安装好了。在运行脚本之前,请使用您喜欢的文本编辑器编辑文件 /usr/lib/python3/dist-packages/jwt/algorithms.py ,然后转到行 143。接着注释掉行 143-146,然后运行脚本。如果您使用的是自己的虚拟机,您可能需要安装 Pyjwt(pip3 install pyjwt)来使用这个脚本。您还需要修改 Pyjwt 库的 algorithm.py 文件,在第 258 行移除 is_ssh_key 条件,因为针对这个漏洞已经发布了补丁。请注意,这个位置可能因虚拟机和安装方式而异。如果您不熟悉库代码编辑,可以使用 jwt.io。一旦验证它正常工作,您就可以修改声明,让自己成为管理员并恢复 flag。
开发错误
这个示例中的错误与示例 3 中的错误类似,但稍微复杂一些。虽然 None 算法是不被允许的,但关键问题在于对称和非对称签名算法都被允许,如下面的示例所示:
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"])
请注意,永远不要混合使用不同的签名算法,因为 decode 函数的 secret 参数可能会被混淆,不知道它是秘密密钥还是公钥。
修复
虽然两种签名算法都可以允许,但需要更多的逻辑来确保不会产生混淆,如下面的示例所示:
header = jwt.get_unverified_header(token)
algorithm = header['alg']
payload = ""
if "RS" in algorithm:
payload = jwt.decode(token, self.public_key, algorithms=["RS256", "RS384", "RS512"])
elif "HS" in algorithm:
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])
username = payload['username']
flag = self.db_lookup(username, "flag")

JWT 的有效期
令牌有效期
在验证令牌签名之前,应计算令牌的有效期,以确保令牌未过期。这通常通过从令牌中读取 exp(过期时间)声明并计算令牌是否仍然有效来完成。
一个常见的问题是如果 exp 值设置得太大(或者根本未设置),令牌的有效期会过长,甚至可能永远不会过期。对于 cookie,cookie 可以在服务器端过期。然而,JWTs 不 具有此相同功能内置。如果我们想在之前过期令牌 exp 过期时间,我们必须保留这些令牌的黑名单,打破使用相同认证服务器的去中心化应用模型。因此,在选择正确的 exp 值时,应考虑到应用的功能。例如,邮件服务器和银行应用之间可能使用不同的 exp 值。
另一种方法是使用刷新令牌。如果你打算测试一个 API 使用 JWT 的,建议你对此做一些研究。
实际示例 6
在这个示例中,JWT 实现没有指定 exp 值,这意味着令牌是永久有效的。使用下面的令牌来恢复你的旗帜:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.ko7EQiATQQzrQPwRO8ZTY37pQWGLPZWEvdWH0tVDNPU
开发错误
如上所述,JWT 没有一个 exp 值,这意味着它是持久的。如果 exp 声明不存在,大多数 JWT 库在验证签名后会将令牌视为有效。
修复
应向声明中添加一个 exp 值。一旦添加,大多数库会在其有效性检查中包含对 JWT 过期时间的审查。这可以按照以下示例所示进行操作:
lifetime = datetime.datetime.now() + datetime.timedelta(minutes=5)
payload = {
'username' : username,
'admin' : 0,
'exp' : lifetime
}
access_token = jwt.encode(payload, self.secret, algorithm="HS256")

跨服务中继攻击
我们将审查的最后一种常见配置错误是跨服务配置错误。如前所述,JWT 通常用于具有集中式认证系统的系统,该系统服务于多个应用程序。然而,在某些情况下,我们可能希望限制哪些应用程序可以使用 JWT 进行访问,尤其是在某些声明仅应适用于特定应用程序时。这可以通过使用观众声明来实现。但是,如果观众声明没有得到正确执行,则可以执行跨服务中继攻击来执行权限提升攻击。
受众声明
JWT 可以包含受众声明。在一个单一的认证系统服务于多个应用程序的情况下,受众声明可以指示 JWT 是为哪个应用程序准备的。然而,这个受众声明的执行必须在应用程序本身上完成,而不是在认证服务器上。如果这个声明没有被验证,由于 JWT 本身通过签名验证仍然被视为有效,它可能会产生未预期的后果。
一个例子是,如果用户在某个应用程序上具有管理员权限或更高角色。分配给用户的 JWT 通常包含一个声明来指示这一点,例如 "admin" : true。然而,同一个用户可能不是在由同一个认证系统服务的另一个应用程序上的管理员。如果在这个第二个应用程序上没有验证受众声明,而该应用程序也使用其管理员声明,服务器可能会错误地认为用户具有管理员权限。这被称为跨服务中继攻击,如下面的动画所示:

让我们来看一个实际例子。
实际例子 7
在这个最后的实际示例中,有两个 API 端点,分别是 example7_appA 和 example7_appB。你可以使用在前面的示例中使用的相同的 GET 请求来恢复旗帜,但你需要将其指向这些端点。此外,对于身份验证,你现在还需要在向 example7 发起的登录请求中包含 "application" : "appX" 数据值。使用以下步骤来执行此示例:
- 使用以下数据段向
example7进行身份验证:'{ "username" : "user", "password" : "password7", "application" : "appA"}'。你会注意到添加了一个观众声明,但你不是管理员。 - 使用此令牌在与
example7_appA和example7_appB进行管理员和用户请求时。你会发现,虽然 appA 接受该令牌,但你不是管理员,而 appB 因为观众不正确而不接受该令牌。 - 使用以下数据段向
example7进行身份验证:'{ "username" : "user", "password" : "password7", "application" : "appB"}'。你会发现,这次添加了观众声明,并且你现在是管理员。 - 使用此令牌再次在两个应用程序上验证自己,看看会发生什么。
现在你可以使用这个来恢复你的旗帜。
开发错误
关键问题是 appA 上没有验证观众声明。这可能是由于观众声明验证已被关闭,或者观众范围设置得太宽。
修复
在解码令牌时,应验证受众声明。这可以按照以下示例完成:
payload = jwt.decode(token, self.secret, audience=["appA"], algorithms="HS256")

结论
在这个房间中,展示了 JWT 实现中的一些常见配置错误和漏洞。总结一下,请注意以下几点:
- JWT 在客户端发送并编码,敏感信息不应存储在其声明中。
- JWT 的安全性仅取决于其签名。在验证签名时应小心谨慎,确保没有混淆或使用弱密钥。
- JWT 应该过期,并具有合理的生命周期,以避免威胁行为者使用持久化的 JWT。
- 在 单点登录 环境 中,观众声明至关重要,以确保特定的应用程序的 JWT 仅在该应用程序中使用。
- 由于 JWT 使用密码学来生成签名,因此密码学攻击也可能与 JWT 利用相关。我们将在我们的密码学模块中更深入地探讨这一点。
- 在这个房间中,我们没有涵盖一个 JWKS 欺骗攻击。如果你有兴趣执行这个漏洞利用,请查看 这个房间 。

浙公网安备 33010602011771号