JWT原理和安全漏洞总结
前言
JWT全称Json Web Token,所以他是Token的一种实现方式
Token的机制
-
客户端输入用户名和密码,发送到服务器端
-
服务器验证用户名和密码,验证成功后签发token返回给客户端
-
客户端将服务器签发的token存储起来
-
此后客户端向服务器获取资源时会携带token
-
服务器收到请求验证token是否正确
传统的session认证方式需要服务器端存储用户的会话信息,消耗内存资源,而通过Token认证例如JWT,服务器端无需存储,只需要验证客户端发送的JWT令牌即可。
Cookie+session方式由于Cookie无法跨域,因此JWT成为了替代方案,多适用于解决单点登录、分布式服务等问题
名词解释
-
JWS:Signed JWT,签名过的JWT
-
JWK:Secret,JWT密钥
-
JWE:Encrypted,经过加密的密文
-
JKU:JWK Set URL,服务器通过访问该URI可以获取JWK集合中的密钥
-
KID:密钥ID,在有多个密钥可供选择的情况下,服务器可以使用这个 ID 来识别正确的密钥
JWT的构成
JWT由三部分构成:头部(Header)、有效载荷(Payload)、签名(Signature),每一部分通过.
分割,如下
红色部分是Header,紫色是Payload,蓝色是Signature
1.Header
typ表示令牌的类型,JWT统一都写为JWT
alg表示签名使用的加密算法,默认为HMAC SHA256
{
"typ": "JWT",
"alg": "HS256"
}
JWT支持的加密算法如下,其中HMAC是对称加密算法,RSA和ECDSA是非对称加密算法
JWS | 算法名称 | 描述 |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
2.Payload
有效载荷部分官方提供了7个字段可供使用
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除此之外用户可以自定义字段,一般可能会存放一些用户的相关信息,例如
{
"name": "Xiaoming",
"admin": "False"
}
3.Signature
签名部分是对前面两部分数据通过指定的加密算法计算Hash值,从而保证数据不被篡改
生成密钥后将Header和Payload部分的内容分别用Base64Url编码并通过.
连接,然后用指定的加密算法和密钥计算Hash值后发送给客户端
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
最后前两部分Base64Url编码后的内容与生成的签名拼接,每个部分通过.
分割,最终组成完整的JWT对象
安全漏洞
1.未验证签名
未对签名进行校验,会导致攻击者随意篡改JWT的内容,从而造成越权等危害
以PortSwagger靶场为例
登录测试账户后我们发现,服务器生成JWT添加到了Cookie
JWT前两部分内容解码,内容如下,sub为当前登录的用户名
访问/admin时提示只允许administrator才能访问
我们把sub的值改为administrator,base64编码后替换掉原来的
发送后成功访问到了/admin页面
2.加密算法验证缺陷
将头部中的"alg"加密算法设置为"none",签名置空,此时构造的任何JWT都是有效的
修改头部的"alg"为none
同样修改"sub"为administrator
将修改完后的内容拼接替换,签名部分直接删掉就可以,但是要保留最后一个.
,维持JWT格式
3.爆破弱密钥
上面提到了JWT支持的加密算法有HMAC、ECDSA、RSA,其中HMAC是对称加密算法,只用一个密钥去加解密
如果使用了HMAC并且密钥是简单的字符串,就可以利用hashcat等工具离线爆破密钥
-a 0
代表指定攻击模式为字典攻击
-m 16500
是指定哈希算法的模式编号,16500对应JWT的HMAC
hashcat -a 0 -m 16500 <jwt> <wordlist>
hashcat会通过给定的密钥字典生成JWT,然后与提供的JWT对比,一致说明加密密钥正确
4.JWT-Header注入
JWT头部有很多参数可能被利用,例如jwk,jku,kid等,如果校验不全可能会受到攻击
4.1 Jwk参数注入
服务器不检查密钥的提供者,我们自己生成RSA算法和公私钥,用生成的私钥签名,公钥放到JWK里发送给服务器(不清楚JWK是什么可以看文首介绍)
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
可以利用Brup的JWK Editor插件一键生成
生成JWK后,回到Repeter,选择Embedded JWK,选择刚才生成的RSA,修改Payload为administrator后,点击Encrypt
替换JWT
4.2 Jku参数注入
服务器接收Jku参数,但是不检查提供的 该URL 是否属于受信任的域
创建Jwk Set,将刚才生成的Jwk公钥,复制到靶场提供的服务器上,注意格式
kid的值与生成的对应,jku添加上返回Jwk Set服务器的地址,payload部分跟之前一样,sub修改为administrator
修改好之后点击Sign,生成JWT
替换JWT,完成后续利用
4.3 Kid参数注入
在JwkSet中可能存在很多密钥,因此用Kid来唯一标识。
服务器接收kid参数,从kid指定的路径获取相关密钥,且使用对称加密。如果kid没有限制格式,配合目录遍历,例如设置kid为Linux中的/dev/null空文件,就会造成用空字符串加密。
在JWT Editor中创建对称加密,密钥设置为AA==也就是0x00空字符。
来到Repeter,修改kid指向"/dev/null",payload中的sub为administrator