JWT 概述
JWT(JSON Web Token)是一种基于 JSON 的开放标准(RFC 7519)实现的令牌(Token)技术,用于在各方之间安全地传输信息。JWT 通常用作对用户进行身份验证并以数字签名和编码的 JSON 对象的形式在各方之间传输信息的手段
解码/验证/生成 jwt --> jwt.io
JWT 分为三部分: 1. Header (标头)
- 描述令牌的元信息,通常包括签名算法(alg,例如 HS256 或 RS256)和类型(typ,通常为 JWT)
- 示例:
{ "alg": "HS256", // 算法名称 "typ": "JWT" // token 类型 }
2. Payload (载荷)
- 声明 (claims),即实际传输的信息
- 声明可以分为三类:
- Registered claims:官方预定义的声明 (非强制) 如 iss(签发人),exp(过期时间),sub(主题)以及 其他
- Public claims:自定义的公共声明
- Private claims:双方约定使用的自定义声明
- 通常任何有权访问令牌的人都可以轻易读取或修改此数据,因此任何基于 JWT 的机制的安全性都十分依赖于加密的签名 (所以不应该在 Payload 存敏感信息)
- 示例:
{ "sub": "1234567890", "name": "koi", "admin": true }
3. Signature (签名)
- Header 和 Payload 是以明文形式传输的,Signature 部分是对前两部分的签名,用于验证消息的完整性和发送者的真实性,防止篡改
- 支持多种签名算法如 HS256, RS256 等
- 签名是直接从令牌的其余部分派生的,所以更改 Header 或 Payload 的单个字节会导致签名不匹配
- 示例:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
以 .
分隔,这三部分经过 base64 编码后拼在一起组成整个 jwt Header.Payload.Signature
服务器通常不会存储有关其发行的 JWT 的任何信息。JWT 一旦被签发,在到期之前始终有效,由于是无状态的,服务端无法直接撤销令牌
常见 JWT 漏洞
漏洞成因
JWT 攻击是指伪造有效 token 从而绕过身份验证以冒充其他用户或提升权限。因为 Header 和 Payload 是可以修改的,所以只要使得 Signature 通过校验就可以伪造
JWT 漏洞通常是由于服务端没有适当处理 JWT,比如不严格校验签名导致 none
算法签名的 JWT 被接受
例如,假设有以下声明的 JWT: {"name": "koi", "isAdmin": false}
。如果服务器根据 isAdmin 字段识别用户身份,那么把其改成 true 并且签名能够通过校验就能冒充管理员身份
信息泄露
JWT 的 Payload 是明文 (Base64 编码) 存储的,任何人可以解码查看。如果将敏感信息存在 Payload 中可能导致信息泄露
重放攻击 / 令牌泄漏
如果站点存在 XSS 漏洞,用户的 token 可能被盗取
由于服务器无法撤销 token,拦截用户的合法 JWT 后,可在有效期内重复发送该 token 冒充用户
如果服务端未正确验证或忽略过期时间,可以重放已过期的令牌
未验证签名
JWT 库通常提供一种验证令牌的方法和另一种仅对其进行解码的方法。例如 node 的jsonwebtoken
库有 verify()
和 decode()
有时开发人员会混淆这两种方法,只将传入的令牌传递给 decode()
,于是服务端不验证签名
弱密钥 (暴力破解)
某些签名算法如 HS256 (HMAC + SHA-256) 使用任意字符串作为密钥。就像密码一样,如果密钥太弱就容易被爆破
少数情况下开发者可能会有疏忽,例如忘记更改默认或占位符密钥。甚至可能直接 cv 网上的代码不改硬编码的密钥,这样很容易被爆
可以使用 hashcat
爆破密钥
hashcat -a 0 -m 16500 <jwt> <wordlist>
hashcat 遍历字典对来自 JWT 的 header 和 payload 进行签名,然后将生成的签名与来自服务器的原始签名进行比较
其他一些工具 (爆破公钥或私钥): https://github.com/silentsignal/rsa_sign2n https://github.com/RsaCtfTool/RsaCtfTool https://github.com/brendan-rius/c-jwt-cracker https://github.com/ticarpi/jwt_tool
算法混淆
算法混淆是指攻击者迫使服务器采用意外的签名算法校验签名,从而伪造有效 JWT 而无需知道签名密钥,通常是由于 JWT 库的实现有缺陷而引起的
其中一种是服务器读取 JWT header 中的 alg 字段来决定签名验证算法
以下伪代码显示了此类 verify()
方法的声明在 JWT 库中的样子的简化示例:
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
}
}
假如开发者使用这个方法时,预期它只处理 RSA256,于是总是将固定的公钥传递给该方法,如下所示:
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);
这样的话只要有公钥,用 HS256 签一个 JWT (header 的 alg 字段为 "HS256") 就能通过校验,而不需要 RS256 私钥
Header 注入
根据 JWS 规范,只有 alg 标头参数是强制性的。但 JWT Header(也称为 JOSE headers)通常包含其他几个参数。攻击者可能会利用以下几个字段
-
jwk(JSON Web Key) 一种将密钥表示为 JSON 对象的标准化格式。示例:
{ "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG", "typ": "JWT", "alg": "RS256", "jwk": { "kty": "RSA", "e": "AQAB", "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG", "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m" } }
服务器应仅使用有限的公钥白名单来验证 JWT 签名,但配置错误的服务器有时会使用 jwk 参数中嵌入的任何密钥
-
jku (JSON Web Key Set URL) 包含表示不同键的 jwk 数组,提供一个 URL,服务器可以从中获取包含正确密钥的一组密钥。示例:
{ "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 解析差异来绕过这种过滤 -
kid (Key ID) 提供一个 ID,服务器可以使用该 ID 在有多个密钥可供选择的情况下识别正确的密钥。但是 JWS 规范没有定义此 id 的具体结构,长什么样由开发者决定,比如指向数据库具体条目或者文件
-
cty (Content Type) 有时用于声明 JWT 负载中内容的媒体类型。通常从标头中省略,但底层解析库无论如何都可能支持它。如果找到了绕过签名验证的方法,则可以尝试注入 cty 标头以将内容类型更改为 text/xml 或 application/x-java-serialized-object,这可能会为 XXE 和反序列化攻击提供新的方向
-
x5c (X.509 Certificate Chain) 有时用于传递用于对 JWT 进行数字签名的密钥的 X.509 公钥证书或证书链。此标头参数可用于注入自签名证书,类似于上面讨论的 jwk 标头注入攻击。由于 X.509 格式及其扩展的复杂性,解析这些证书也可能会引入漏洞。有关更多详细信息可看 CVE-2017-2800 和 CVE-2018-2633
这些用户可控的参数分别告诉接收服务器在验证签名时使用哪个密钥。那么如果有服务器配置不当,可能就会把这些参数提供的密钥拿去用
JWT attacks Lab
未验证签名
靶场链接: Lab: JWT authentication bypass via unverified signature
目标是进入管理面板删除 carlos
用户
给了账号密码是 wiener
peter
,先登上去,服务器会给我们一个 JWT (响应头的 set-cookie
字段或者请求头的 Cookie
字段)
页面有一个 /admin
接口,直接访问会提示只有 administrator
才有访问权限
把刚才的 JWT 复制到 jwt.io,把 sub
字段从 wiener
改成 administrator
(什么算法都可以),再复制生成的 JWT (服务端不验证签名,所以签名长什么样都无所谓)
回到 /admin
接口,请求头带上 Cookie: session=eyJraWQiOiJlMDQxZDE1Ni1jZjMzLTQ4NTYtYThmYi04OGE0ZDFlMWI5ZjgiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTczNTU1MDc1OCwic3ViIjoiYWRtaW5pc3RyYXRvciJ9.dmQ6Ra2lFLFjzCB3B0_hNR4D-0UahKrSgGdpvzIa2Rk
即可进入管理面板,删除 carlos 用户即可通关
此外也可以用 BurpSuite 的 JWT Editor 插件直接改 administrator
,不用来回复制,可以更方便
有缺陷的签名验证
靶场链接: Lab: JWT authentication bypass via flawed signature verification
登录后转到 /admin
接口抓包,使用 JWT Editor 插件把 alg
字段改成 none
,sub
字段改成 administrator
,去掉 Signature,Forward 请求即可
弱密钥
靶场链接: Lab: JWT authentication bypass via weak signing key
下载弱密钥字典
curl "https://raw.githubusercontent.com/wallarm/jwt-secrets/refs/heads/master/jwt.secrets.list" > jwt-secrets-list.txt
把 JWT 复制过来,使用 hashcat 爆破
hashcat -a 0 -m 16500 "eyJraWQiOiJmN2ZmYWE3YS0wODliLTRkZWEtOGZjZS1lYmNkNDQ4MWFmODkiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTczNDk2MDMzMiwic3ViIjoid2llbmVyIn0.DCkpKxQ-0gDhlsO3m7o7STtzqVa4pkovy8cWQnfc8A" jwt-secrets-list.txt
原来的 JWT 粘贴到 jwt.io,修改成 administrator
,再把爆出来的密钥填进去生成新的就可以拿去用了
jwk header 注入
靶场链接: https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jwk-header-injection
JWT Editor 提供了一个便于测试漏洞的 jwk 一键注入
从前面不难发现删除操作是访问固定的 URL 带查询参数,所以登录后直接访问 /admin/delete?username=carlos
抓包 ,先改成 administrator
,然后来到上方 JWT Editor 选项,创建一个 RSA Key
然后点击 Attack -> Embedded JWK,选择刚刚创建的 key
Forward 请求即可成功删除用户
jku header 注入
靶场链接: Lab: JWT authentication bypass via jku header injection
靶场页面上方提供了一个攻击服务器
先把 body 改成
{
"keys": [
]
}
登录后访问 /admin/delete?username=carlos
抓包,先改成 administrator
,然后来到上方 JWT Editor 选项,上一关生成过一个 key (或者生成新的 RSA Key),右键它,点 Copy Public Key as JWK,粘贴到刚刚攻击服务器的 body 的 keys 中,再点 Store,获得一个 URL
回到刚刚抓的包那里,把 kid
字段改成刚刚 URL 存的 kid
,添加一个 jku
字段为 URL,最后用之前用的 key 签一下 (选 Don't modify header) 转发请求即可成功删除用户
kid header 注入
靶场链接: Lab: JWT authentication bypass via kid header path traversal
登录后把 JWT 粘贴到 jwt.io,把 wiener 改成 administrator。kid 字段改成 ../../../../../../../dev/null
(目录遍历到一个空文件),密钥改成 AA==
(空字符)(勾选 secret base64 encoded)
带上新的 JWT 前去删除用户