什么是JWT

JSON Web Token(JWT) 是一种标准化格式,用于在系统之间发送加密签名的 JSON 数据。与经典会话令牌不同,服务器所需的所有数据都存储在 JWT 本身的客户端中。

JWT 格式

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

https://jwt.io/

1
2
3
1.头部(Header)包含了关于该 JWT 的元数据,如令牌的类型("typ")和签名算法("alg")等。
2.有效负载(Payload)是 JWT 的主要内容,包含了用于验证和识别用户身份的声明信息,如用户ID、过期时间、权限等。
3.签名(Signature)是使用私钥对头部和载荷进行签名,以确保令牌的完整性和真实性。在服务器端验证 JWT 时,使用相应的公钥进行解密和验签,以确保令牌的合法性。

image-20231124111054340

JWT攻击

1
2
3
4
5
6
7
JWT(JSON Web Token)本身并不越权,但是如果在使用和验证 JWT 的过程中存在安全漏洞或错误的实现,可能导致越权问题。
以下是一些可能导致JWT越权问题的常见情况:

1、未校验签名算法: JWT未校验签名算法或者攻击者可将签名算法改为none
2、使用了弱密钥: 攻击者可以爆破获得密钥,然后使用密钥通过有效签名对令牌进行重新签名
3、jwk,jku密钥注入
......

通过Burp Suite靶场学习JWT攻击

一、通过未经验证的签名绕过 JWT 身份验证(JWT authentication bypass via unverified signature)

1
2
JWT 库通常提供一种验证令牌的方法和另一种仅对其进行解码的方法。例如,Node.js 库jsonwebtoken有verify()和decode()。
有时,开发人员会混淆这两种方法,只将传入的令牌传递给该decode()方法。这实际上意味着应用程序根本不验证签名。

1、开启实验环境,使用账号登录抓包

image-20231124131251634

2、将访问目录改为/admin,然后尝试访问,可以看到服务器返回401 Unauthorized,代表身份验证未通过,并且在JWT的Payload部分有用户名信息

image-20231124131343994

3、将用户名wiener改为administrator,,现在我们就可以成功访问到管理员面板

image-20231124131522611

4、然后使用管理员权限删除对应的用户即可

image-20231124131640195

二、通过有缺陷的签名验证绕过 JWT 身份验证(JWT authentication bypass via flawed signature verification)

1
2
3
JWT 标头包含一个alg参数。这告诉服务器使用哪种算法对令牌进行签名,以及在验证签名时需要使用哪种算法。
{"alg": "HS256","typ": "JWT"}
JWT 可以使用一系列不同的算法进行签名,但也可以不签名。在这种情况下,该alg参数设置为none,表示所谓的“不安全的 JWT”。

1、开启实验环境,使用账号登录抓包

image-20231124132639471

2、将访问目录改为/admin,然后尝试访问,可以看到服务器返回401 Unauthorized,代表身份验证未通过,并且在JWT的Payload部分有用户名信息

image-20231124132742549

3、将头部中的alg改none,将用户名wiener改为administrator,现在我们就可以成功访问到管理员面板(因为设置了签名验证参数alg为none,所以最后的签名就没有意义的,将签名部分删除,记得不要删掉最后的.)

image-20231124133216295

4、然后使用管理员权限删除对应的用户即可

image-20231124133336734

三、通过弱签名密钥绕过 JWT 身份验证(JWT authentication bypass via weak signing key)

1
在实现 JWT 应用程序时,如果开发人员使用了弱密钥,这会导致攻击者可以很容易的猜解出对应的密钥,然后使用密钥通过有效签名对令牌进行重新签名。

1、开启实验环境,使用账号登录抓包

image-20231124133954237

2、将访问目录改为/admin,然后尝试访问,可以看到服务器返回401 Unauthorized,代表身份验证未通过

image-20231124134049563

3、使用hashcat爆破密钥

1
hashcat -a 0 -m 16500  eyJraWQiOiI1M2Y1ZjAxMi03NjI3LTRlNzQtODhlNS0zY2JkY2VmY2E0YWYiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTcwMDgwNzk3OX0.EOjDT3rM6jGbdWFNHeS5RyVDg9-vfswjfjiiipeBZ4U secret.txt

image-20231124135852988

可以看到爆破出密钥为secret1

4、利用密钥通过有效签名对令牌进行重新签名

image-20231124140210844

5、成功访问到管理员面板

image-20231124140320820

6、然后使用管理员权限删除对应的用户即可

image-20231124140537820

四、通过 jwk 标头注入绕过 JWT 身份验证(JWT authentication bypass via jwk header injection)

1
2
3
4
5
6
根据JWS规范,只有alg标头参数是强制的。但实际上,JWT 标头(也称为 JOSE 标头)通常包含其他几个参数。攻击者特别感兴趣的是以下内容。
jwk(JSON Web Key) - 提供表示密钥的嵌入式 JSON 对象。
jku(JSON Web 密钥集 URL)- 提供一个 URL,服务器可以从中获取包含正确密钥的一组密钥。
kid(密钥 ID)- 提供一个 ID,服务器可以使用该 ID 在有多个密钥可供选择的情况下识别正确的密钥。根据密钥的格式,这可能具有匹配的kid参数。

这些用户可控的参数分别告诉服务器在验证签名时使用哪个密钥。那么如何利用这些来注入,使用自己的任意密钥而不是服务器的密钥签名的修改后的JWT。

通过 jwk 参数注入自签名 JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JSON Web 签名 (JWS) 规范描述了一个可选的jwk标头参数,服务器可以使用该参数以 JWK 格式将其公钥直接嵌入到令牌本身中。

JWK(JSON Web 密钥)是一种将密钥表示为 JSON 对象的标准化格式。
您可以在以下 JWT 标头中看到这样的示例:
{
"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。

1、开启实验环境,使用账号登录抓包

image-20231124141158514

2、将访问目录改为/admin,然后尝试访问,可以看到服务器返回401 Unauthorized,代表身份验证未通过

image-20231124141341773

3、使用jwk注入,使用JWT Editor插件生成新的 RSA 密钥

image-20231124144227348

4、将用户名修改为administrator,然后注入刚才生成的RSA密钥

image-20231124143710058

5、注入完成后,发送请求,可以看到已经可以成功访问管理员面板了

image-20231124144044299

6、然后使用管理员权限删除对应的用户即可

image-20231124144150358

五、通过 jku 标头注入绕过 JWT 身份验证(JWT authentication bypass via jku header injection)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
jwk某些服务器允许您使用(JWK Set URL) 参数来引用包含密钥的 JWK Set,而不是直接使用标头参数嵌入jku公钥。当验证签名时,服务器从该 URL 中获取相关密钥。

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

{
"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 集有时会通过标准端点公开公开,例如/.well-known/jwks.json.
更安全的网站只会从受信任的域获取密钥,但有时您可以利用 URL 解析差异来绕过这种过滤

1、开启实验环境,使用账号登录抓包

image-20231124150609833

2、将访问目录改为/admin,然后尝试访问,可以看到服务器返回401 Unauthorized,代表身份验证未通过

image-20231124150705574

3、通过jku注入,使用JWT Editor插件生成新的 RSA 密钥

image-20231124150813735

4、进入服务器,将Body改为

1
2
3
4
5
{
"keys": [

]
}

image-20231124150948405

5、将生成的RSA密钥内容复制到Body中

1
2
3
4
5
6
7
8
9
10
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "61d4d9d5-be2b-4416-bc79-fc20a0adf238",
"n": "0BQgIUMaTJSEt1zKAfur-yY5KjK2guvHrTb1BMeFdNvlP8icq7JPbCVElVYQ1cj4WSatExWEaXsdRlRsxdMmS9V9UgF1ziVxn3QU6zx6cDMGek0X4QxK9HmyUXjnroGpj1KpzsWrwDmN-EvfS8F-vPBRuJlbJqlfvEOtWGn1XnCSPfrRrN7CCndgTQ_IxoelWhhsEj7MWfLZtiqez5lV829wawRB7Cdd2jy95QjwitxysiOGLvre6CFh-ctmSjGehMMTKd7au9y2g96T-_DRIhCH0hHBRDCxa5LbAN8UkxRuBuTI4IM87aZofmK8zbsllryIit9C1dOHB4n_t1Wz-w"
}
]
}

image-20231124151153279

6、将JWT的头部修改为如下(当验证签名时,服务器从会直接从jku指定的URL中获取相关密钥)

1
2
3
4
5
{
"kid": "61d4d9d5-be2b-4416-bc79-fc20a0adf238",
"alg": "RS256",
"jku": "https://exploit-0a50009503b9adf080d4843e019f00ee.exploit-server.net/exploit"
}

image-20231124152103966

7、然后选择刚才生成的RSA密钥对JWT进行重新签名

image-20231124151611645

8、重新签名之后,发送请求,就可以成功访问管理员面板了

image-20231124152847144

9、然后使用管理员权限删除指定账户即可

image-20231124152950727

六、通过 Kid 标头路径遍历绕过 JWT 身份验证(JWT authentication bypass via kid header path traversal)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
通过kid参数注入自签名JWT
服务器可能使用多个加密密钥来签署不同类型的数据,而不仅仅是 JWT。因此,JWT 的 header 中可能会包含一个kid(Key ID)参数,该参数可以帮助服务器识别在验证签名时使用哪个密钥。

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

{
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}
如果服务器还支持使用对称法签名的 JWT,则这尤其危险。在这种情况下,攻击者可能会将kid参数指向可预测的静态文件,然后使用与该文件内容匹配的密钥对 JWT 进行签名。

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

1、开启实验环境,使用账号登录抓包

image-20231124163103958

2、将访问目录改为/admin,然后尝试访问,可以看到服务器返回401 Unauthorized,代表身份验证未通过

image-20231124163133760

3、生成对称密钥

image-20231124163256880

4、将生成的属性值替换k为 Base64 编码的空字节 ( AA==)。(这只是一种解决方法,因为 JWT Editor插件不允许使用空字符串签署令牌)

image-20231124163436977

5、在 JWT 的 头部,将kid参数的值更改为指向文件的/dev/null序列:

1
../../../dev/null

image-20231124163626800

6、然后使用生成的对称密钥对JWT重新签名

image-20231124163825139

7、重新签名之后发送请求,就可以成功访问管理员面板了

image-20231124163931101

8、然后使用管理员权限删除指定的用户即可

image-20231124164030822

如何防御JWT攻击

1
2
3
4
5
6
7
8
来自BurpSuite的建议:
1、使用最新的库来处理JWT,并确保开发人员对相关安全问题足够了解。现代代码库的使用降低了在代码实现中引入安全漏洞的可能性,但由于相关规范固有的灵活性,也并非万无一失。
2、确保对收到的任何JWT进行严格的签名验证,并考虑边缘情况,如使用非预期的算法签名的JWT。
3、为jku头部提供允许主机白名单,并严格执行。
4、确保不会受到kid头部参数路径穿越或SQL注入的影响。
5、为发行的任何令牌设置到期日期
6、尽可能避免在 URL 参数中发送令牌
7、使发行服务器能够撤销令牌(例如,在注销时)