目录
JSON Web 令牌 (JWT) 是一种标准化格式,用于在系统之间发送加密签名的 JSON 数据。理论上,它们可以包含任何类型的数据,但最常用于发送有关用户的信息(“声明”),作为身份验证、会话处理和访问控制机制的一部分。
与经典会话令牌不同,所有数据服务器所需的数据存储在客户端 JWT 本身内。这使得 JWT 成为高度分布式网站的热门选择,用户需要与多个后端服务器无缝交互。
推荐JWT利用工具:https://github.com/ticarpi/jwt_tool
一、JWT 的结构
JWT由三部分组成:header、payload和signature,JWT 的header和payload部分只是 base64编码的 JSON 对象。
标头包含有关令牌本身的元数据,而有效负载包含有关用户的实际“声明”。例如,您可以从下面的令牌中解码有效负载得到以下声明:
在大多数情况下,任何有权访问令牌的人都可以轻松读取或修改此数据。因此,任何基于JWT 的机制的安全性都严重依赖于加密签名。
二、JWT signature
发出令牌的服务器通常通过散列标头和有效负载来生成签名。在某些情况下,他们还会对生成的哈希值进行加密。无论哪种方式,此过程都会涉及秘密签名的密钥。此机制为服务器提供了一种方法来验证令牌中的数据自发布以来没有被篡改:
1、由于签名是直接从令牌的其余部分派生的,因此更改标头的单个字节或有效负载导致签名不匹配。
2、如果不知道服务器的秘密签名的密钥,则不可能为给定标头或有效负载生成正确的签名。
可以通过以下在线工具对JWT令牌进行调试:
https://jwt.io/
https://www.bejson.com/jwt/
目前来说,我们所说的JWT 通常指的是 JWS 或 JWE 令牌。当人们使用术语“JWT”时,他们几乎总是指 JWS 令牌。JWE 也是一样的,只是令牌的实际内容是加密的,而不仅仅是编码的。
JWT 规范是由 JSON Web Signature (JWS) 和 JSON Web Encryption (JWE) 规范扩展而来的,它们定义了实际实现 JWT 的具体方法。
三、常见的JWT令牌的漏洞利用方式
1、有缺陷的JWT认证机制
接受任意signature
针对JWT,Node.js 库的 jsonwebtoken 有 verify() 和decode()两个函数。有时,开发人员会混淆这两个方法,只将传入的令牌传递给decode() 方法,而没有传递给verify()函数。这实际上就意味着应用程序根本不需要验证客户端传递过来的签名。
原数据包,低权用户不具备/admin接口的访问权限:
由于系统未对传入的JWT的签名进行验证,所以可以通过更改payload中的字段实现越权访问——具备删除功能点的使用
接受没有signature的令牌
JWT 的header中包含 alg 参数。这告诉服务器使用哪种算法对令牌进行签名,以及在验证签名时需要使用哪种算法。
{ "alg": "HS256", "typ": "JWT" }
这本质上是有缺陷的,因为服务器别无选择,只能被动的信任来自令牌用户的可控输入,但此时该令牌根本尚未经过验证。换句话说,攻击者可以直接影响服务器检查令牌是否可信的方式。
JWT 可以使用一系列不同的算法进行签名,同样也可以不签名。在这种情况下,alg参数设置为none,这表示所谓的“不安全的JWT”。由于这样做的明显危险,服务器通常会拒绝没有签名的令牌。但是,由于这种过滤依赖于字符串解析,因此您有时可以使用经典的混淆技术(例如混合大写和编码)来绕过这些过滤器。
原数据包,存在header、payload和signature三个字段,且header中存在验证签名的方式,payload中也有token用户。
正常访问/admin接口是没有权限的
修改payload中的sub的值为administrator也无法访问
于是就可以考虑删除签名,将alg的值改为none(有些系统会对该字段进行过滤,可以考虑使用一些经典的混淆技术进行绕过),将signature部分删除。
注意,删除了signature的情况下要保留payload部分后面的(.)
2、暴力破解加密密钥
某些签名算法,例如 HS256 (HMAC + SHA-256),使用任意独立字符串作为密钥。就像密码一样,这一秘密不太可能轻易被攻击者猜出或暴力破解。否则,他们可能能够使用他们喜欢的任何标头和有效负载值创建 JWT,然后使用密钥以有效签名重新签署令牌。
但是在实现 JWT 应用程序时,开发人员有时会忘记更改默认值或者一些占位符。他们甚至也有可能复制并粘贴在网上找到的代码片段,然后忘记更改作为示例提供的硬编码机密。在这种情况下,攻击者就可以使用众所周知的秘密的单词列表来暴力破解服务器的加密密钥。
使用hashcat暴力破解密钥
字典:https://github.com/wallarm/jwt-secrets/blob/master/jwt.secrets.list
kali自带hashcat
hashcat -a 0 -m 16500 <jwt> <wordlist>
破解后的结果
访问一下网址进行对jwt的修改
https://jwt.io/
https://www.bejson.com/jwt/
在signature部分填写破解出来的密钥,点击校验后可以发现签名有效
将payload部分的wiener修改为administrator,然后访问/admin接口,就可以正常访问了
还可以用BP插件商城里JWT Editor对JWT进行修改
3、JWT中的header参数注入
根据 JWS 规范,只有 alg 标头参数是强制性的。但实际上,JWT 的header(也称为 JOSE 标头)通常包含以下其他几个参数,它们都有可能被攻击者使用。
jwk(JSON Web Key)- 是一种将密钥表示为 JSON 对象的标准化格式。
jku(JSON Web Key set URL)- 提供一个 URL,服务器可以从中获取包含正确密钥的一组密钥。
kid(Key ID)- 提供一个 ID,服务器可以使用该 ID 在有多个密钥可供选择的情况下识别正确的密钥。根据密钥的格式,这可能具有匹配的kid参数。
这些用户可控的参数分别告诉接收服务器在验证签名时使用哪个密钥。
(1)、通过 jwk 参数注入自签名
JSON Web 签名 (JWS) 规范描述了一个可选的 jwk 标头参数,服务器可以使用该参数以 JWK 格式将其公钥直接嵌入到令牌本身中。
你有可能在 JWT 的header中看到这样的示例:
理想情况下,服务器应该仅仅使用有限的公钥白名单来验证 JWT 签名。但是,配置错误的服务器有时会使用 jwk 参数中嵌入的任何密钥。
你可以通过使用自己的 RSA 私钥对修改后的 JWT 进行签名,然后将匹配的公钥嵌入到 jwk 标头中来利用此行为。
你可以在Burp中手动添加或修改jwk参数,也可以使用JWT编辑器来测试此漏洞:
首先抓包获取JWT
访问/admin接口依旧是无权访问
点击JWT Editor插件,生成一个RSA密钥,依次操作保存生成的密钥
再回到/admin接口的数据包,将JWT Web Token打开,将wiener修改为administrator后点击Attack攻击
选择Embedded JWK,然后点击发送,就可以成功访问到/admin接口了
你也可以通过手动将 jwk 参数添加到 JWT 的标头来嵌入 JWK,而不是使用 JWT 编辑器扩展中的内置攻击。
但是在这种情况下,你就还需要更新令牌的 kid 标头以匹配嵌入密钥的 kid。
(2)、通过 jku 参数注入自签名
某些服务器允许您使用 jku(JWK Set URL)标头参数来引用包含密钥的 JWK Set,而不是直接使用 jwk 标头参数嵌入公钥。当验证签名时,服务器从该 URL 中获取相关密钥。
jku格式:
{
"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 集有时会通过标准的API接口公开,例如/.well-known/jwks.json。更安全的网站只会从受信任的域获取密钥,但有时你可以利用 URL 解析差异来绕过这种过滤。例如:SSRF。
在这种环境下首先使用repeater重放自己的登录数据包:
再选择JWT Editor插件生成一套RSA密钥(直接生成,不用管密钥大小)
点击生成一份公钥
再选择一个远端的服务器,上传一份恶意文件,文件内容为:
{
"keys": [
]
}
将生成的公钥粘贴进去,接着尝试访问,看看能否正常访问。
回到repeater,点击JWT web Token,修改如下内容:
1、将header中的kid字段修改为上面生成的公钥中的kid。
2、添加jku键,键值为恶意文件的访问连接。
3、将低权用户名修改为高权用户名
点击sign,选择自己生成的RSA私钥来签名,以此实现JWT自签名的注入
注入完成后访问/my-account?id=administrator,可以发现成功以administrator的身份完成了访问。
(3)、通过kid参数注入自签名
服务器可能使用多个加密密钥来签署不同类型的数据,而不仅仅是 JWT。因此,JWT 的 header 中可能会包含一个kid(Key ID)参数,该参数可以帮助服务器识别在验证签名时使用哪个密钥。
验证密钥通常存储为 JWK 集。在这种情况下,服务器可以简单地查找具有与令牌相同kid的JWK。然而,JWS 规范并未定义此 ID 的具体结构, 它只是开发人员选择的任意字符串。例如,他们可能使用kid参数来指向数据库中的特定条目,甚至是文件名。
所以kid有可能会受到目录遍历的攻击,如果此参数也容易受到目录遍历的影响,则攻击者可能会强制服务器使用其文件系统中的任意文件作为验证密钥。
如果服务器还支持使用对称算法签名的 JWT,那么攻击者可能会将 Kid 参数指向一个存在于固定目录的静态文件,然后使用与该文件内容匹配的密钥对 JWT 进行签名。
理论上,你可以对任何文件执行此操作,但最简单的方法之一是使用/dev/null,
它存在于大多数 Linux 系统上。由于这是一个空文件,读取它会返回一个空字符
串。因此,使用空字符串对令牌进行签名将得到有效的签名。
具体操作如下:首先使用burp抓取自己账号下的数据包
点击JSON Web Token,修改header中的kid值为“../../../dev/null”,payload中的sub也要修改为高权限的用户。
由于我们知道/dev/null目录是一个空文件,kid的参数改为它后会导致服务器会使用该文件的内容来对JWT签名进行验证,所以我们签名的密钥就是空(Base64加密为`AA==`)
将生成的新的JWT令牌,复制替换掉原有的令牌,即可访问/my-account?id=administrator接口。
1、除了访问可以预测内容的文件外,还可以
尝试"kid":"/dev/tcp/yourIP/yourPort"来测试连接性,甚至也可以使用
一些SSRF的有效负载
2、如果服务器将其验证密钥存储在数据库中,则 Kid 标头参数也是 SQL 注入
攻击的潜在向量。
3、Linux中"/proc/sys/kernel/randomize_va_space",它的值为2,使用2
作为密钥对JWT令牌进行签名。
4、在使用“kid”在数据库中检索密钥的场景中, 你可以将"kid"参数中的有效负载
更改为 : `non-existent-index' UNION SELECT 'ATTACKER';-- -` ,
然后使用 `ATTACKER`作为密钥对JWT令牌进行签名(针对kid参数的SQL注入)。
(4)、其它可以利用的 JWT -header参数
cty(内容类型)参数
有时用于声明 JWT 负载中内容的媒体类型。这通常从标头中省略,但底层解析库无论如何都可能支持它。
如果你找到了绕过签名验证的方法,则可以尝试注入 cty 标头以将内容类型更改为 text/xml 或 application/x-java-serialized-object,这可能会为 XXE 和反序列化攻击启用新的向量。
X5U、X5C URL 操作
X5C
有时用于传递用于对 JWT 进行数字签名的密钥 X.509 公钥证书或证书链。此标头参数可用于注入自签名证书,类似于上面讨论的 jwk 标头注入攻击。由于 X.509 格式及其扩展的复杂性,解析这些证书也可能会引入漏洞。
该参数可能包含base64 格式的证书:
如果攻击者生成自签名证书并使用相应的私钥创建伪造的令牌,并将“x5c”参数的值替换为新生成的证书并修改其他参数,即 n、e 和 x5t,那么基本上伪造的令牌将被接受由服务器。
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -out attacker.crt
openssl x509 -in attacker.crt -text
详细信息,请查看 CVE-2017-2800 和 CVE-2018-2633
X5U
X.509 URL。指向一组以 PEM 形式编码的 X.509(证书格式标准)公共证书的 URI。集合中的第一个证书必须是用于签署此 JWT 的证书。后续证书均签署前一个证书,从而完成证书链。
利用方式:
1、尝试将此标头更改为您控制下的 URL ,并检查是否收到任何请求
2、要使用您控制的证书伪造新令牌,您需要创建证书并提取公钥和私钥
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -out attacker.crt
openssl x509 -pubkey -noout -in attacker.crt > publicKey.pem
3、然后,可以通过https://jwt.io使用创建的公钥和私钥创建新的 JWT ,并将参数 x5u 指向创建的证书 .crt。
(5)、嵌入式公钥 (CVE-2018-0114)
如果 JWT 嵌入了公钥,如下例
使用以下nodejs脚本可以从该数据生成公钥:
const NodeRSA = require('node-rsa');
const fs = require('fs');
n ="ANQ3hoFoDxGQMhYOAc6CHmzz6_Z20hiP1Nvl1IN6phLwBj5gLei3e4e-DDmdwQ1zOueacCun0DkX1gMtTTX36jR8CnoBRBUTmNsQ7zaL3jIU4iXeYGuy7WPZ_TQEuAO1ogVQudn2zTXEiQeh-58tuPeTVpKmqZdS3Mpum3l72GHBbqggo_1h3cyvW4j3QM49YbV35aHV3WbwZJXPzWcDoEnCM4EwnqJiKeSpxvaClxQ5nQo3h2WdnV03C5WuLWaBNhDfC_HItdcaZ3pjImAjo4jkkej6mW3eXqtmDX39uZUyvwBzreMWh6uOu9W0DMdGBbfNNWcaR5tSZEGGj2divE8";
e = "AQAB";
const key = new NodeRSA();
var importedKey = key.importKey({n: Buffer.from(n, 'base64'),e: Buffer.from(e, 'base64'),}, 'components-public');
console.log(importedKey.exportKey("public"));
可以生成新的私钥/公钥,将新的公钥嵌入到令牌中并使用它来生成新的签名:
openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key
使用以下nodejs脚本获取“n”和“e”:
const NodeRSA = require('node-rsa');
const fs = require('fs');
keyPair = fs.readFileSync("keypair.pem");
const key = new NodeRSA(keyPair);
const publicComponents = key.exportKey('components-public');
console.log('Parameter n: ', publicComponents.n.toString("hex"));
console.log('Parameter e: ', publicComponents.e.toString(16));
最后,使用公钥和私钥以及新的“n”和“e”值,可以使用https://jwt.io使用任何信息伪造一个新的有效 JWT。
(6)、JTI(JWT ID)
JTI (JWT ID) 声明为 JWT 令牌提供唯一标识符。它可用于防止令牌被重放。
然而,ID 的最大长度为 4 (0001-9999) 。请求 0001 和 10001 将使用相同的 ID。因此,如果后端在每个请求上递增 ID,就可以滥用它来重播请求(需要在每次成功重播之间发送 10000 个请求)。
4、JWT算法混淆
即使服务器使用你无法暴力破解的可靠密钥,你仍然可以通过使用开发人员未预料到的算法对令牌进行签名来伪造有效的 JWT
基础步骤:
1、获取服务器的公钥
2、将公钥转换为合适的格式
3、使用修改后的负载和设置为 HS256 的 alg 标头创建恶意 JWT。
4、使用公钥作为令牌,使用 HS256 对令牌进行签名。
在获取公钥的过程中会出现下面这种情况:
服务器有时会通过映射到 /jwks.json 或 /.well-known/jwks.json 的标准端点将其公钥公开为 JSON Web Key (JWK) 对象。这些可以存储在键值为 JWK的数组中(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"
}
]
}
存在公开的公钥
Step-1 获取公钥
访问/jwks.json 或 /.well-known/jwks.json获取被公开的JWT公钥
将keys中的一个值复制出来(确保没有复制任何多余的符号),且确认kid的参数是否与JWT中的kid一致。
将点击JWT Editor中的New RSA Key去生成一个非对称密钥,将复制的公钥粘贴进去,选择JWK后点击确认。
以PEM的格式复制公钥
即可获得公钥
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo5WdI7ZRbmRXHDCQ+hGw
+KCMq4FU5HqwtCWjiOiJ31XfpoSIGJ3dYzYphyFxqR3haJS0inKlDNIVYe7j8BIg
kWH/TFroojkqrVlGHwhV6DNkWOBj/RaYYoYLbksW7+uLAFoBEWbg+V88y8Bf/gXC
E85xibxOFIOPU0EYKxdZqn6PzlfQwhjf4Lm5J6FwJWgfO9pwByWrT4ZJ6ADhOmMO
iw5ppUTEV/5DKYZTx56kgQk7IsOdPyyxkycyPcgPti9ymZqedZ7cCpEHXL0AsHNK
agFNIpzW7OxL5bZ2/q8oQy5d0JapdBJF1J4cfCrM9UX20fn+NeMCMMw3UcrC7ZGz
3wIDAQAB
-----END PUBLIC KEY-----
Step-2 将公钥转化为合适的格式
对获取的公钥进行base64编码
再次点击JWT Editor的New Symmetric key去生成对称的密钥,将其中的k的值替换为base64编码后的公钥。
Step-3 修改JWT
对数据包的header和payload进行修改,首先注意kid值是否与公钥的kid一致,其次对alg的值进行修改,将非对称加密RS256修改为HS256.
将payload中的sub修改为administrator
Step-4 签名制作令牌
使用插件对数据包中的JWT令牌进行签名,选用的对称签名密钥就是之前生成的(内含公钥的base64编码)
就可以正常访问/admin接口
也可以将编码后公钥放置到https://jwt.io/中,以此来通过签名生成新的令牌。替换旧的令牌后就可以访问/admin接口。
从现有JWT令牌中提取公钥
即使密钥没有公开公开,您也可以从一对现有的 JWT 中提取它。
Step-1 获取令牌
重复登录自己的账号两次,获取两个有效的且不同的JWT令牌
Step-2 暴力破解服务器的公钥
从docker中拉取工具portswigger/sig2n,使用如下命令对公钥进行暴力破解
sudo docker run --rm -it portswigger/sig2n <token1> <token2>
初次使用会去拉取容器
拉取完毕即可进入暴力破解
这时候会发现,输出包含一个或多个 n 的计算值。其中每一项在数学上都是可能的,但只有其中一项与服务器使用的值匹配。
在每种情况下,输出还提供以下内容:
1、X.509 和 PKCS1 格式的 Base64 编码公钥。
2、使用这些公钥生成的被篡改的JWT,一条公钥对应一个被篡改的JWT。
使用每一条被篡改的JWT来访问/my-account接口,直至出现200响应
访问第三条时出现200响应,说明这是正确的 X.509 密钥。
修改payload中的sub为administrator,并使用对应的X.509 密钥(base64编码后的)去签名生成新的令牌
接着使用新的令牌去访问/admin接口,即可绕过JWT的身份验证