JWT总结
介绍
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
构成
JWT由3个部分构成:
头部(header)
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
{ 'typ': 'JWT', 'alg': 'HS256' }
然后将头部进行base64加密(该加密是可以对称解密的), 构成了第一部分
载荷(payload)
载荷就是存放有效信息的地方,包含三个部分:
- 标准中注册的声明(建议但不强制使用)
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不 建议添加敏感信息,因为该部分在客户端可解密。
- 私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
然后将其进行base64加密,得到Jwt的第二部分。
签名(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了 jwt的第三部分。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt 的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。
一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
一般我们遇到jwt字符串可以到https://jwt.io/
这个网站解密。
Jwt安全问题
敏感信息泄露
我们能够轻松解码payload和header,因为这两个都只经过Base64Url编码,而有的时候开发者会误将敏感信息存在payload中。
未校验签名
某些服务端并未校验JWT签名,所以,可以尝试修改signature后(或者直接删除signature)看其是否还有效。
签名算法可被修改为none(CVE-2015-2951)
头部用来声明token的类型和签名用的算法等,比如:
{
"alg": "HS256",
"typ": "JWT"
}
以上header指定了签名算法为HS256
,意味着服务端利用此算法将header和payload进行加密,形成signature,同时接收到token时,也会利用此算法对signature进行签名验证。
但是如果我们修改了签名算法会怎么样?比如将header修改为:
{
"alg": "none",
"typ": "JWT"
}
那么服务端接收到token后会将其认定为无加密算法, 于是对signature的检验也就失效了,那么我们就可以随意修改payload部分伪造token。
用none算法生成的JWT只有两部分了,根本连签名都不存在。
签名密钥可被爆破
jwt使用算法对header和payload进行加密,如果我们可以爆破出加密密钥,那么也就可以随意修改token了。
这里有一个python版本的jwt爆破脚本:https://github.com/Ch1ngg/JWTPyCrack
也可以快速用以下脚本爆破:
jwt_str = "xxx.ttt.zzz"
path = "D:/keys.txt"
alg = "HS256"
with open(path,encoding='utf-8') as f:
for line in f:
key_ = line.strip()
try:
jwt.decode(jwt_str,verify=True,key=key_,algorithm=alg)
print('found key! --> ' + key_)
break
except(jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
print('found key! --> ' + key_)
break
except(jwt.exceptions.InvalidSignatureError):
continue
else:
print("key not found!")
修改非对称密码算法为对称密码算法(CVE-2016-10555)
JWT的签名加密算法有两种,对称加密算法和非对称加密算法。
对称加密算法比如HS256,加解密使用同一个密钥,保存在后端。
非对称加密算法比如RS256,后端加密使用私钥,前端解密使用公钥,公钥是我们可以获取到的。
如果我们修改header,将算法从RS256更改为HS256,后端代码会使用RS256的公钥作为HS256算法的密钥。于是我们就可以用RS256的公钥伪造数据
伪造密钥(CVE-2018-0114)
jwk是header里的一个参数,用于指出密钥,存在被伪造的风险。比如CVE-2018-0114: https://cve.mitre.org/cgi-bin/c
攻击者可以通过以下方法来伪造JWT:删除原始签名,向标头添加新的公钥,然后使用与该公钥关联的私钥进行签名。
比如:
{
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"kid": "TEST",
"use": "sig",
"e": "AQAB",
"n": "oUGnPChFQAN1xdA1_f_FWZdFAis64o5hdVyFm4vVFBzTIEdYmZZ3hJHsWi5b_m_tjsgjhCZZnPOLn-ZVYs7pce__rDsRw9gfKGCVzvGYvPY1hkIENNeBfSaQlBhOhaRxA85rBkg8BX7zfMRQJ0fMG3EAZhYbr3LDtygwSXi66CCk4zfFNQfOQEF-Tgv1kgdTFJW-r3AKSQayER8kF3xfMuI7-VkKz-yyLDZgITyW2VWmjsvdQTvQflapS1_k9IeTjzxuKCMvAl8v_TFj2bnU5bDJBEhqisdb2BRHMgzzEBX43jc-IHZGSHY2KA39Tr42DVv7gS--2tyh8JluonjpdQ"
}
}
kid指定攻击
kid 即为 key ID ,存在于 jwt header 中,是一个可选的字段,用来指定加密算法的密钥
如图,在头部注入新的 kid 字段,并指定 HS256 算法的 key 为 1,生成新的 jwt_json
jwt.encode({"name":"admin","id":1},key="1",algorithm='HS256',headers={"kid":"1"})
验证没有问题:
如果 server 端开启了头部审查,那么此方法也将没有效果
另外,可以构造 kid 进行 SQL注入、任意文件读取、命令执行等攻击,但是除了 CTF 中会有这种强行弱智写法,实际案例可以说是并不存在,实用性极其低,故不再赘述。
重放JWT
如果特定令牌只能使用一次怎么办?让我们想象一个场景,当用户编写一个生成的令牌以执行我们API中的DELETE方法时。然后,例如一年后(理论上他不再拥有相应的权限)之后,他尝试再次使用它(所谓的重播攻击)。
为此,请使用以下声明:jti和exp。Jti(JWT ID)是令牌标识符,必须是唯一的,而exp是令牌到期日期的定义。这两个字段的组合将使我们在适当程度上缩短令牌的有效性及其唯一性。
小结
Header部分
- 是否支持修改算法为none/对称加密算法
- 删除签名
- 插入错误信息
- kid字段是否有SQL注入/命令注入/目录遍历
- jwk元素是否可信
- 是否强制使用白名单上的加密算法
Payload部分
- 其中是否存在敏感信息
- 检查过期策略,比如
exp
,iat
Signature
- 检查是否强制检查签名
- 密钥是否可以爆破
- 是否可以通过其他方式拿到密钥
其他
- 重放
- 通过匹配校验的时间做时间攻击
- 修改算法RS256为HS256
- 弱密钥破解