JWT的安全问题

JWT介绍

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。JWT常被用于前后端分离,可以和Restful API配合使用,常用于构建身份认证机制。
在身份验证中,当用户使用他们的凭证成功登录时,JSON Web Token将被返回并且必须保存在本地(通常在本地存储中,但也可以使用Cookie),而不是在传统方法中创建会话服务器并返回一个cookie。
无论何时用户想要访问受保护的路由或资源,用户代理都应使用承载方案发送JWT,通常在授权header中。这是一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。服务器受保护的路由将在授权头中检查有效的JWT,如果存在,则允许用户访问受保护的资源。由于JWT是独立的,所有必要的信息都在那里,减少了多次查询数据库的需求。
这使我们可以完全依赖无状态的数据API,无论哪些域正在为API提供服务,因此跨源资源共享(CORS)不会成为问题,因为它不使用Cookie。

JWT的解析

JWT构造与解析的网站
https://jwt.io/

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoicjFkZDFlciIsInByaXYiOiJhZG1pbiJ9.vNsfeJu7tJNoxOXnqBvezp7WFqgWF5oLL0ZOhQz2vlY

JWT是由三部分构成的,分别是header,payload,signature,中间用点进行连接。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

解码后 {"alg":"HS256","typ":"JWT"}

alg表示算法,这里使用的是HS256算法,即HMACSHA256,是对称加密的,只有一个密钥。还有一种常用的加密算法是RS256,即RSASHA256,便是非对称加密。

typ表示类型,这里也就是表示是JWT。

Payload

eyJuYW1lIjoicjFkZDFlciIsInByaXYiOiJhZG1pbiJ9

解码后{"name":"r1dd1er","priv":"admin"}

这一部分是用户自定义的数据,由于可以直接base64解码查看,所以不要将敏感信息放在此部分。

Signature

1
2
3
4
5
 HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
test
)

最后的签名部分是将前两部分的内容加上我们定制的密钥一起进行相应的加密,这里的test就是相应的密钥。然后生成的加密字符串便是JWT的第三部分。

JWT的安全问题

密钥爆破

如果使用的是HS256算法,由于加密与解密共用一个密钥,因此如果密钥被设置的太过简单,被爆破出来后便可任意伪造凭证进行登录。

现成的JWT密钥破解工具
https://github.com/brendan-rius/c-jwt-cracker

1
2
./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoicjFkZDFlciIsInByaXYiOiJhZG1pbiJ9.vNsfeJu7tJNoxOXnqBvezp7WFqgWF5oLL0ZOhQz2vlY
Secret is "test"

算法修改

算法HS256使用秘钥对每条消息进行签名和验证。
算法RS256使用私钥对消息进行签名,并使用公钥进行验证。
如果将算法从RS256更改为HS256,后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。
由于公钥有时可以被攻击者获取到,所以攻击者可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。
后端代码会使用RSA公钥+HS256算法进行签名验证。
即更改算法为HS256,此时即不存在公钥私钥问题,因为对称密码算法只有一个key,等于说我们掌握了密钥

还有一种是将header部分的alg改为none,一些JWT库也支持none算法,即不使用签名算法。当alg字段为空时,后端将不执行签名验证。将alg字段改为none后,系统就会从JWT中删除相应的签名数据(这时,JWT就会只含有头部 + ‘.’ + 有效载荷 + ‘.’),然后将其提交给服务器。

1
2
{"alg":"none","typ":"JWT"}
base64:eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=

密钥可控

假如存在header头部分是下面这样的场景

1
2
3
4
5
{
"typ":"JWT",
"alg":"sha256",
"kid":"8201"
}

这里的kid便是我们可控的部分,类似逻辑可能为

1
2
sql="select * from table where kid=$kid"
res=exec(sql)

原本的逻辑应该是kid作为编号去数据库中查询相应的密钥,现在构造kid = 0 union select 1,使得res=1,这样就控制了查询结果为1,也就是密钥直接被我们指定为1,然后构造时将1作为密钥即可伪造任意消息

参考链接

https://www.anquanke.com/post/id/145540#h3-8

https://xz.aliyun.com/t/2338