什么是 JWT?

JSON web tokens (JWT) 是一种标准化格式,用于在系统之间发送加密签名的 JSON 数据。理论上,它们可以包含任何类型的数据,但最常用于发送有关用户的信息(“声明”),作为身份验证、会话处理和访问控制机制的一部分。

与经典会话令牌不同,服务器所需的所有数据都存储在 JWT 本身的客户端中。

JWT 格式

JWT 由 3 部分组成:标头、有效负载和签名。它们均由点分隔,如以下示例所示:

1
eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm9sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA

JWT 的标头和有效负载部分只是 base64url 编码的 JSON 对象。标头包含有关令牌本身的元数据,而有效负载包含有关用户的实际“声明”。例如,您可以从上面的令牌中解码有效负载以揭示以下声明:

1
2
3
4
5
6
7
8
9
{
"iss": "portswigger",
"exp": 1648037164,
"name": "Carlos Montoya",
"sub": "carlos",
"role": "blog_author",
"email": "carlos@carlos-montoya.net",
"iat": 1516239022
}

在大多数情况下,任何有权访问令牌的人都可以轻松读取或修改此数据。因此,任何基于 JWT 的机制的安全性都严重依赖于加密签名

签名

发出令牌的服务器通常通过散列标头和有效负载来生成签名。在某些情况下,他们还会对生成的哈希值进行加密。无论哪种方式,此过程都涉及秘密签名密钥。此机制为服务器提供了一种方法来验证令牌中的数据自发布以来没有被篡改:

  • 由于签名是直接从令牌的其余部分派生的,因此更改标头或有效负载的单个字节会导致签名不匹配。
  • 如果不知道服务器的秘密签名密钥,就不可能为给定的标头或有效负载生成正确的签名。

注意:https://jwt.io/

什么是 JWT 攻击?

JWT 攻击的影响通常很严重。如果攻击者能够使用任意值创建自己的有效令牌,他们可能能够升级自己的权限或冒充其他用户,完全控制他们的帐户。

注意:一旦 JWT 被签发,就无法撤销。即使在 JWT 还未过期的情况下,如果需要立即使其失效,是无法做到的;也就是说即使你的账号logout,但是曾经的jwt令牌依然有效;

JWT 攻击的漏洞是如何产生的?

JWT 漏洞通常是由于应用程序本身的 JWT 处理缺陷而引起的。与 JWT 相关的各种规范在设计上相对灵活,允许网站开发人员自行决定许多实现细节。即使使用久经考验的库,这也可能导致他们意外引入漏洞。

这些实现缺陷通常意味着 JWT 的签名未得到正确验证。这使得攻击者能够篡改通过令牌的有效负载传递给应用程序的值。即使签名得到了可靠的验证,它是否真正可信在很大程度上依赖于服务器的秘密密钥是否保密。如果该密钥以某种方式泄露,或者可以被猜测或暴力破解,则攻击者可以为任何任意令牌生成有效签名,从而损害整个机制

利用有缺陷的 JWT 签名验证

接受任意签名

JWT 库通常提供一种验证令牌的方法和另一种仅对其进行解码的方法。例如,Node.js 库jsonwebtokenverify()decode()

有时,开发人员会混淆这两种方法,只将传入的令牌传递给该decode()方法。这实际上意味着应用程序根本不验证签名。也就是说,后端服务器并没有使用verify()函数对令牌进行验证,而是直接使用decode函数解码

接受没有签名的令牌

其中,JWT 标头包含一个alg参数。这告诉服务器使用哪种算法对令牌进行签名,以及在验证签名时需要使用哪种算法。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

这本质上是有缺陷的,因为服务器别无选择,只能隐式信任来自令牌的用户可控输入,而此时该令牌根本尚未经过验证。换句话说,攻击者可以直接影响服务器如何检查令牌是否可信。

JWT支持“none”算法。如果将alg字段设置为“none”,则任何令牌都将被视为有效。

暴力破解密钥

某些签名算法,例如 HS256 (HMAC + SHA-256),使用任意独立字符串作为密钥。就像密码一样,这一秘密可能轻易被攻击者猜出或暴力破解,

Tools:

https://github.com/ticarpi/jwt_tool

https://github.com/brendan-rius/c-jwt-cracker.git

1
./jwtcrack <token>

JWT 标头参数注入

根据JWS规范,只有alg标头参数是强制的。但实际上,JWT 标头通常包含其他几个参数。攻击者特别感兴趣的是以下内容。

  • jwk- 提供表示密钥的嵌入式 JSON 对象。
  • jku- 提供一个 URL,服务器可以从中获取包含正确密钥的一组密钥。
  • kid- 提供一个 ID,服务器可以使用该 ID 在有多个密钥可供选择的情况下识别正确的密钥。根据密钥的格式,这可能具有匹配的kid参数。

通过 jwk 参数注入自签名 JWT

JWS规范描述了一个可选的jwk标头参数,服务器可以使用该参数以 JWK 格式将其公钥直接嵌入到令牌本身中。

JWT 标头示例:

1
2
3
4
5
6
7
8
9
10
11
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}

在理想情况下,服务器应该是只使用公钥白名单来验证JWT签名的,但对于一些相关配置错误的服务器会用JWK参数中嵌入的任何密钥进行验证,攻击者就可以利用这一行为,用自己的RSA私钥对修改过的JWT进行签名,然后在JWK头部中嵌入对应的公钥进行越权操作

实验如下

实验环境来自:https://portswigger.net/

使用:wiener:peter登陆 ,没有权限访问/admin

使用jwk注入步骤:

  1. 生成新的 RSA 密钥
  2. 请注意,您无需选择密钥大小,因为稍后会自动更新。

此时标头已经成功注入jwk;再次发送请求包之后,可以成功拿到adminstrator权限

1
2
3
4
5
6
7
8
9
10
11
{
"kid": "45287eda-91e5-420e-9477-95856bbfd5f2",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "45287eda-91e5-420e-9477-95856bbfd5f2",
"n": "nNJZA3RKyKP8fsXTZ9BSKlOEKM7HSo6HbaSKiDhK70mzlqHQmgAjYjFLpq-hsmlZp8oY34kLDCnMfYgNhRZlNoadPxwy7tWdUklawuQPVvbAlPbY9gIXaDpu-7h1P0bm_8uw3MtmX5PPhW7-mFysBAHb-EJIryU5xFueFuZVIJzQ1yeepb6j7IgbjDcGzO_XP0zoXQBTpJTBYlpUaAhr_5fjKiHM6LaXgQYKIcImquEDbqo9Z4N1QJYmaLZH256Ki-AP6cav6uu0erYO1eejrDJ41bg34Sd-IzaN-Rr9v2kbDoLxd_jh041uXMz0UNBVbRHnCqB6kIbG-w1O0g2Jcw"
}
}

通过 jku 参数注入自签名 JWT

有些服务器并不会直接使用JWK头部参数来嵌入公钥,而是使用JKU(JWK Set URL)来引用一个包含了密钥的JWK Set,当验证签名时,服务器从该 URL 中获取相关密钥;我们就可以借此来构造一个密钥从而实现越权操作

JWK Set 是一个 JSON 对象,包含表示不同键的 JWK 数组。您可以在下面看到这样的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
}

实验如下

右键单击刚刚生成的密钥条目,然后选择将公钥复制为 JWK

实验已经提供了:exploit-server;将公钥复制的JWK放到Body部分

1
2
3
4
5
{
"keys": [

]
}

把kid替换为我们生成的kid,将jkw写出exploit-server地址;然后将改成administrator,再用一开始生成的RSA签名即可

注意:如果你用的burpsuit是盗版的话,使用插件可能sign不成功;我最终是用kali自带的社区版解决的;

通过kid参数注入自签名JWT

服务器可能使用多个加密密钥来签署不同类型的数据,而不仅仅是 JWT。因此,JWT 的 header 中可能会包含一个kid(Key ID)参数,该参数可以帮助服务器识别在验证签名时使用哪个密钥。

验证密钥通常存储为 JWK 集。在这种情况下,服务器可以简单地查找与kid令牌相同的JWK。然而,JWS 规范并未定义此 ID 的具体结构 - 它只是开发人员选择的任意字符串。例如,他们可能使用kid参数来指向数据库中的特定条目,甚至是文件名。

如果此参数也容易受到目录遍历的攻击,则攻击者可能会强制服务器使用其文件系统中的任意文件作为验证密钥。

1
2
3
4
5
6
{
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}

如果服务器还支持使用对称算法签名的 JWT,则这尤其危险。在这种情况下,攻击者可能会将kid参数指向可预测的静态文件,然后使用与该文件内容匹配的密钥对 JWT 进行签名。

理论上,您可以对任何文件执行此操作,但最简单的方法之一是使用/dev/null,它存在于大多数 Linux 系统上。由于这是一个空文件,读取它会返回一个空字符串。因此,使用空字符串对令牌进行签名将得到有效的签名。

实验如下

新建对称密钥;请注意,您无需选择密钥大小,因为稍后会自动更新。将生成的属性值替换k为 Base64 编码的空字节 ( AA==)。

将参数的值更改kid为指向/dev/null文件,修改为administrator;单击Sign,然后选择在上一部分中生成的对称密钥即可

JWT算法混淆攻击

当攻击者能够强制服务器使用与网站开发人员预期不同的算法来验证 JSON Web 令牌 ( JWT ) 的签名时,就会发生算法混淆攻击(也称为密钥混淆攻击)。如果这种情况处理不当,攻击者可能会伪造包含任意值的有效 JWT,而无需知道服务器的秘密签名密钥。

算法混淆漏洞是如何产生的?

算法混乱漏洞通常是由于 JWT 库的实现有缺陷而引起的。尽管实际的验证过程因所使用的算法而异,但许多库提供了一种与算法无关的单一方法来验证签名。这些方法依赖于alg令牌标头中的参数来确定它们应执行的验证类型。

以下伪代码显示了此泛型verify()方法的声明在 JWT 库中的简化示例:

1
2
3
4
5
6
7
8
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
if(algorithm == "RS256"){
// Use the provided key as an RSA public key
} else if (algorithm == "HS256"){
// Use the provided key as an HMAC secret key
}
}

当随后使用此方法的网站开发人员假设它将专门处理使用 RS256 等非对称算法签名的 JWT 时,就会出现问题。由于这个有缺陷的假设,他们可能总是将固定的公钥传递给该方法,如下所示:

1
2
3
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);

在这种情况下,如果服务器收到使用 HS256 等对称算法签名的令牌,则库的通用verify()方法会将公钥视为 HMAC 秘密。这意味着攻击者可以使用 HS256 和公钥对令牌进行签名,并且服务器将使用相同的公钥来验证签名。

注意:用于签署令牌的公钥必须与服务器上存储的公钥完全相同。这包括使用相同的格式(例如 X.509 PEM)

执行算法混淆攻击

算法混淆攻击通常涉及以下高级步骤:

  1. 获取服务器的公钥
  2. 将公钥转换为合适的格式
  3. 创建一个恶意 JWT,其负载经过修改,alg标头设置为HS256.
  4. 使用公钥作为密钥,使用 HS256 对令牌进行签名。

可以获取服务器的公钥非常重要;例如,服务器有时会通过映射到/jwks.json 的 标准端点将其公钥公开为 JSON Web Key (JWK) 对象。/.well-known/jwks.json这些可以存储在名为 的 JWK 数组中keys。这称为 JWK 集。

注意:私钥用于解密数据或生成数字签名;而公钥用于加密数据或验证数字签名

实验如下:

访问jwks.json拿到公钥,

1
2
3
4
5
6
7
8
9
10
11
12
{
"keys":[
{
"kty":"RSA",
"e":"AQAB",
"use":"sig",
"kid":"0db5403e-6f79-47e5-aa30-b005a59dc7be",
"alg":"RS256",
"n":"mVx2PicKCcRTrUZ-2rcpVVGjUedQie9HKP8coUmvWY7eKHb7XoHZVj9kD79CW5bRtipLV27NzeunNwtbXfYRTVRD33C6oDF393Q0czJc63EXmMCE6p2MWKpsieVhPe5XjK0L7UGm1BGSGafMndQrjdNv2V1eMnz5oX1AsSwltMDkZD_wFrhVxBXO71GmHfm4jWYoJzchr1Z5X_DCM6Q_0cWZ4PLl9GVeCZwiZg-5SvGvhwEOeKqnJriulWIubqCouY2ygOVNbmMK0_L8SPHXfntZ4JXm0pvRbV-gufizH7vnAJfJ7O7dMc6AVTqSNnWNWIgp1_Z8VQ253WkM77dpEw"
}
]
}

并转化成pem格式,可以利用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
import base64
import binascii
import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

jwk_data = {
"keys":[
{
"kty":"RSA",
"e":"AQAB",
"use":"sig",
"kid":"0db5403e-6f79-47e5-aa30-b005a59dc7be",
"alg":"RS256",
"n":"mVx2PicKCcRTrUZ-2rcpVVGjUedQie9HKP8coUmvWY7eKHb7XoHZVj9kD79CW5bRtipLV27NzeunNwtbXfYRTVRD33C6oDF393Q0czJc63EXmMCE6p2MWKpsieVhPe5XjK0L7UGm1BGSGafMndQrjdNv2V1eMnz5oX1AsSwltMDkZD_wFrhVxBXO71GmHfm4jWYoJzchr1Z5X_DCM6Q_0cWZ4PLl9GVeCZwiZg-5SvGvhwEOeKqnJriulWIubqCouY2ygOVNbmMK0_L8SPHXfntZ4JXm0pvRbV-gufizH7vnAJfJ7O7dMc6AVTqSNnWNWIgp1_Z8VQ253WkM77dpEw"
}
]
}

for key in jwk_data["keys"]:
modulus = int.from_bytes(base64.urlsafe_b64decode(key["n"] + "==="), "big")
exponent = int.from_bytes(base64.urlsafe_b64decode(key["e"] + "==="), "big")
public_numbers = rsa.RSAPublicNumbers(exponent, modulus)
public_key = public_numbers.public_key()
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print(pem.decode())

或者burp插件

新建 RSA 密钥,复制到key框中,再选择pem单选框或者右键单击刚刚创建的密钥条目,然后选择将公钥复制为 PEM(注意这里最后有个换行符)

对此 PEM 密钥进行 Base64 编码,然后复制生成的字符串

新建对称密钥。在对话框中,单击“生成”以生成 JWK 格式的新密钥。请注意,您无需选择密钥大小,因为稍后会自动更新;将 k 属性的生成值替换为您刚刚创建的 Base64 编码的 PEM。

alg参数的值更改为HS256。将声明的值更改subadministrator。单击Sign,然后选择在上一部分中生成的对称密钥即可;

从现有令牌派生公钥

在公钥不易获得的情况下,您仍然可以通过从一对现有 JWT 派生密钥来测试算法混淆。使用诸如 之类的工具,此过程相对简单jwt_forgery.py。您可以在rsa_sign2nGitHub 存储库上找到此脚本以及其他几个有用的脚本。

我们还创建了该工具的简化版本,您可以将其作为单个命令运行:

1
docker run --rm -it portswigger/sig2n <token1> <token2>

这将使用您提供的 JWT 来计算 的一个或多个潜在值n。不要太担心这意味着什么 - 您需要知道的是只有其中之一n与服务器密钥使用的值相匹配。对于每个潜在值,我们的脚本输出:

  • X.509 和 PKCS1 格式的 Base64 编码 PEM 密钥。
  • 使用每个密钥签名的伪造 JWT。

要识别正确的密钥,请使用 Burp Repeater 发送包含每个伪造 JWT 的请求。服务器仅接受其中之一。然后,您可以使用匹配密钥来构造算法混淆攻击。

实验如下:

获取服务器生成的两个 JWT(登陆第一次,拿到一个jwt令牌后退出,再登陆第二次,拿到第二个jwt令牌)

在终端中,运行以下命令,并传入两个 JWT 作为参数。

1
docker run --rm -it portswigger/sig2n <token1> <token2>

请注意,输出包含一个或多个 的计算值n。其中每一项在数学上都是可能的,但只有其中一项与服务器使用的值匹配。

返回到 Burp Repeater 中的请求并将路径更改回/my-account;将会话 cookie 替换为这个新的 JWT,然后发送请求;发现第二个成功返回200;说明成功;

从终端窗口中,复制您在上一部分中确定为正确的 Base64 编码的 X.509 密钥。请注意,您需要选择密钥,而不是上一节中使用的被篡改的 JWT。