常见的 JWT 攻击类型:
- 敏感信息泄露:攻击者可能通过各种手段获取到 JWT 中包含的敏感信息,例如用户身份、权限等。
- 接受任意签名:如果服务器在验证 JWT 时没有正确检查签名,攻击者可以创建一个带有任意签名的 JWT 并获得未经授权的访问。
- 接受未签名的令牌:如果服务器允许未签名的 JWT,攻击者可以创建一个未签名的 JWT 并获得未经授权的访问。
- 暴力破解密钥:攻击者可以通过暴力破解的方式尝试猜测用于签名 JWT 的密钥。
- JWT 头部参数注入:攻击者可以通过篡改 JWT 头部的参数(如 alg、jwk、jku、kid 等)来绕过身份验证机制。
- 算法混淆攻击:攻击者可以通过混淆 JWT 使用的加密算法来绕过身份验证机制。
这种缺陷通常意味着 JWT 的签名未得到正确验证。使攻击者能够篡改通过令牌的有效负载传递给应用程序的值。即使签名经过可靠验证,它是否真的可信在很大程度上也取决于服务器的密钥是否保持机密。如果此密钥以某种方式泄露,或者可以被猜到或暴力破解,攻击者可以任何任意令牌生成有效签名,从而破坏整个机制。
优点是
- 无状态:服务器不需要存储会话信息,这增加了系统的可扩展性和可用性,减轻了服务端的压力。
- 易于分布式部署:由于 JWT 是自包含的,可以在多个服务器之间共享,而不需要依赖中央存储。
- 跨域支持:JWT 可以在不同域名之间传递,支持跨域身份验证和授权。
注:JWT 在分布式部署中的优势
由于 JWT 包含了所有必要的信息,服务器在验证 JWT 时不需要查询数据库或任何中央存储。这使得 JWT 非常适合在分布式系统中使用。
在传统的会话管理中,服务器需要维护每个用户的会话数据,这些数据通常存储在内存或数据库中。在分布式系统中,多个服务器实例需要共享会话数据,这通常通过集中式存储(如 Redis)或会话粘滞(session sticky)等技术来实现,增加了系统的复杂性和维护成本。
而JWT 不需要依赖中央存储。每个服务器实例都可以独立验证和处理 JWT,这大大简化了系统的架构和维护工作。所以 JWT 的无状态可以实现任何服务器实例都可以验证和处理用户请求,无需共享会话数据,
JWT(JSON Web Token)库通常提供两种主要的方法来处理 JWT 令牌:验证令牌和解码令牌。验证令牌的方法(如 Node.js 库 jsonwebtoken 中的 verify() 方法)不仅解码 JWT,还会检查令牌的签名是否有效。确保了令牌的内容没有被篡改,并且令牌是由可信的发行者签发的。解码令牌的方法(如 Node.js 库 jsonwebtoken 中的 decode() 方法)只解码 JWT 的头部和有效载荷,而不验证签名。这意味着解码令牌的方法只能用于查看令牌的内容,而不能保证令牌的有效性和安全性。开发人员有时会混淆这两个方法,只将传入的令牌传递给 decode() 方法。这实际上意味着应用程序根本不验证签名。
然而,这种机制本质上是有缺陷的,因为服务器必须默认信任来自令牌的用户可控输入。意味着攻击者可以通过操纵 alg 参数来影响服务器验证令牌的方式。
例如,攻击者可以将 alg 参数设置为 none,这表示所谓的“不安全的 JWT”,即令牌没有签名。尽管服务器通常会拒绝没有签名的令牌,但由于这种筛选依赖于字符串解析,攻击者有时可以使用经典的混淆技术(例如混合大小写和意外编码)来绕过这些筛选条件。
开发人员有时会使用一些开源系统或库,但是会忘记更改默认密钥(默认密钥通常是系统或库在初始化时自动生成的密钥。如某些库可能会使用固定的字符串作为默认密钥)或占位符密钥(用于在开发过程中模拟真实密钥的行为,不是随机生成的,而是使用简单的字符串或者其他容易识别的模式。例如,在示例代码中,可能会使用“secret”或者“password”作为占位符密钥)。甚至可能会复制和粘贴网上找到的代码片段,然后忘记更改作为示例提供的硬编码密钥(直接在代码中写入密钥。一旦代码被泄露,密钥也会随之暴露。硬编码密钥难以更改,因为每次更改都需要修改代码并重新部署)。在这种情况下,攻击者使用已知密钥的单词列表暴力破解服务器的密钥可能很简单。
- jwk(JSON Web 密钥)- 提供表示密钥的嵌入式 JSON 对象。
- jku (JSON Web 密钥集 URL) - 提供一个 URL,服务器可以从该 URL 中获取包含正确密钥的一组密钥。
- kid (Key ID) - 提供一个 ID,在有多个密钥可供选择的情况下,服务器可以使用该 ID 来识别正确的密钥。根据键的格式,这可能具有匹配的 kid 参数。
这些用户可控制的参数都告诉服务器在验证签名时使用哪个密钥。攻击者可能会利用这些来注入使用任意密钥(而不是服务器密钥)签名的修改后的 JWT。
一般情况下,服务器应仅使用有限的公钥白名单来验证 JWT 签名。但是,配置错误的服务器有时会使用 jwk 参数中嵌入的任何密钥。可以通过使用自己的 RSA 私有密钥对修改后的 JWT 进行签名,然后将匹配的公有密钥嵌入 jwk 标头来利用此行为。
某些服务器允许使用 jku (JWK Set URL) 标头参数来引用包含密钥的 JWK 集(一个 JSON 对象,其中包含表示不同键的 JWK 数组),而不是直接使用 jwk 标头参数嵌入公钥。验证签名时,服务器会从此 URL 获取相关密钥。
JWK(JSON Web Key)是一种用于表示公钥的 JSON 对象格式。JWK 集是一组 JWK 的集合,通常用于在不同系统之间共享公钥。许多网站会通过标准端点公开 JWK 集,并且只从受信任的域获取密钥。但有时可以利用 URL 解析差异来绕过此类过滤。例如,某些系统可能对 URL 的解析不够严格,导致可以使用不同的 URL 形式来访问相同的资源。这种情况下,攻击者可能会尝试构造特殊的 URL 来绕过域名过滤,从而获取本应受限的密钥。
kid 参数没有具体的结构,可以选择任意字符串,可以使用 kid 参数来指向数据库中的特定条目,甚至是文件的名称。
如果kid参数也容易受到目录遍历的攻击,则攻击者可能会强制服务器使用其文件系统中的任意文件作为验证密钥。
如果服务器还支持使用对称算法签名的 JWT,在这种情况下,攻击者可能会将 kid 参数指向可预测的静态文件,然后使用与此文件内容匹配的密钥对 JWT 进行签名。
可以对任何文件执行签名操作,但使用 /dev/null 是最简单的方法之一。由于 /dev/null 返回一个空字符串,因此使用它对令牌进行签名将产生有效的签名。(注:在 Linux 系统中,/dev/null 是一个特殊文件,通常被称为“黑洞”。任何写入 /dev/null 的数据都会被丢弃,而读取 /dev/null 则会返回一个空字符串。)
如果服务器将其验证密钥存储在数据库中,则 kid 标头参数也是 SQL 注入攻击的潜在载体。
- cty(内容类型)- 有时用于声明 JWT 负载中内容的媒体类型。这通常在 Headers 中省略,但底层解析库可能仍然支持它。如果您找到了绕过签名验证的方法,则可以尝试注入 cty 标头以将内容类型更改为 text/xml 或 application/x-java-serialized-object ,这可能会为 XXE 和反序列化攻击启用新的向量。
- x5c(X.509 证书链)- 有时用于传递 X.509 公钥证书或用于对 JWT 进行数字签名的密钥的证书链。此标头参数可用于注入自签名证书,类似于上面讨论的 jwk 标头注入攻击。由于 X.509 格式及其扩展的复杂性,解析这些证书也可能引入漏洞。
算法混淆漏洞通常是由于 JWT 库的实现存在缺陷而出现的。尽管实际的验证过程因使用的算法而异,但许多库都提供了单一的、与算法无关的方法来验证签名。这些方法依赖于令牌标头中的 alg 参数来确定它们应执行的验证类型。
- 获取服务器的公钥
服务器有时会通过映射到 /jwks.json 或 /.well-known/jwks.json 的标准端点将其公有密钥作为 JSON Web 密钥 (JWK) 对象公开。这些可以存储在称为 keys 的 JWK 数组中。这称为 JWK 集(JWK 集是一个 JSON 对象,其中包含表示不同密钥的 JWK 数组。每个 JWK 对象代表一个单独的密钥,可以包括密钥类型、算法、使用场景等信息。)。即使密钥未公开,也可以从一对现有 JWT 中提取它。
- 将公钥转换为合适的格式
服务器可能会以 JWK 格式公开其公钥,但在验证令牌的签名时,它将使用其本地文件系统或数据库中的密钥副本。这可能以不同的格式存储。为了使攻击起作用,用于对 JWT 进行签名的密钥版本必须与服务器的本地副本相同。除了采用相同的格式外,每个字节都必须匹配,包括任何非打印字符。
- 创建恶意 JWT,并修改有效负载,将 alg 标头设置为 HS256。
获得合适格式的公钥后,可以根据需要修改 JWT。只需确保 alg 标头设置为 HS256 即可。
- 使用 HS256 对令牌进行签名,使用公钥作为密钥。
使用 HS256 算法对令牌进行签名,并将 RSA 公钥作为密钥。
原文始发于微信公众号(老付话安全):JWT漏洞攻击详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论