JWT 简介
JSON Web Token(JSON Web 令牌)是一个开放标准 (rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以 JSON 对象安全地传输信息。通过 JSON 形式作为 Web 应用中的令牌,用于在各方之间安全地将信息作为 JSON 对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
传统 session
HTTP 协议本身是一种无状态的协议,即使用户向服务器提供了用户名和密码来进行用户认证,在下次请求时用户也得再一次进行用户认证。因为根据 HTTP 协议,服务器并不能知道接收到的请求来自哪个用户,所以为了让应用能识别是哪个用户发出的请求,只能在服务器存储─份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 Cookie 以便下次请求时发送给应用。这样应用就能识别请求来自哪个用户。
存在问题
- 用户经改应用认证后,应用都要在服务端存储一份 session。而 session 一般都是保存在内存中,随着认证用户的增多,服务端的开销会明显增大。而且用户下次的请求还必须发送到这台服务器上,这样才能拿到授权的资源。在分布式应用上会限制负载均衡器的能力。
- session 是基于 cookie 来进行用户识别,cookie 如果被截获,用户很容易受到 CSRF(跨站伪造请求攻击)攻击。
JWT 认证
认证流程:
- 前端通过 Web 表单将自己的用户名和密码发送到后端的接口。该过程一般是 HTTP 的 POST 请求。建议的方式是通过 SSL 加密的传输 (https 协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的 id 等其他信息作为 JWT Payload (负载),将其与头部分别进行 Base64 编码拼接后签名,形成一个 JWT (Token)。
- 后端将 JWT 字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在 localStorage(浏览器本地缓存)或 sessionStorage(session 缓存)上,退出登录时前端删除保存的 JWT 即可。
- 前端在每次请求时将 JWT 放入 HTTP 的 Header 中的 Authorization 位。(解决 XSS 和 XSRF 问题)
后端检查是否存在,如存在验证 JWT 的有效性。例如,检查签名是否正确﹔检查 Token 是否过期;检查 Token 的接收方是否是自己 (可选) - 验证通过后后端使用 JWT 中包含的用户信息进行其他逻辑操作,返回相应结果
JWT 结构
先放一个可以提供 JWT 验证的网站:https://jwt.io/
JWT 是一个字符串,由三部分组成,中间用 .
隔开
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphcmVuIiwiYWRtaW4iOnRydWV9.J3Vcpqx76LFtxe8xTMBORxZydb2YnPsMcSHq8cdSRww
第一部分是头部(Header),第二部分是有效载荷(Payload),第三部分是签名(Signature)
其 secret 为:helloctf
1、头部(Header)
头部包含两部分信息:
- 声明类型
- 声明加密的算法。通常直接使用 HMAC、SHA256、RSA。
{ "alg": "HS256", "typ": "JWT"}
然后将头部进行 base64 加密,构成第一部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- 注意:可以将 JWT 中的 alg 算法修改为 none:
JWT 支持将算法设定为 “None”。如果 “alg” 字段设为 “ None”,那么 JWT 的第三部分会被置空,这样任何 token 都是有效的。这样就可以伪造 token 进行随意访问。
2、有效载荷(Payload)
包含 3 部分信息:
- 标准中注册的声明(建议但不强制使用)
iss
: jwt 签发者sub
: jwt 所面向的用户aud
: 接收 jwt 的一方exp
: jwt 的过期时间,这个过期时间必须要大于签发时间nbf
: 定义在什么时间之前,该 jwt 都是不可用的.iat
: jwt 的签发时间jti
: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
- 公共的声明
公共的声明可以添加任何的信息。 - 私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。
举例一个 payload:
{ "sub": "1234567890", "name": "Jaren", "admin": true}
然后将其进行 base64 加密,得到 Jwt 的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphcmVuIiwiYWRtaW4iOnRydWV9
3、签证(Signature)
包含以下三个部分:
- base64 加密后的 header
- base64 加密后 payload
- 密钥 secret
这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用.
连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
// javascriptvar encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); //J3Vcpqx76LFtxe8xTMBORxZydb2YnPsMcSHq8cdSRww
通过 JWT 进行认证
客户端接收服务器返回的 JWT,将其存储在 Cookie 或 localStorage 中。此后,客户端将在与服务器交互中都会带 JWT。如果将它存储在 Cookie 中,就可以自动发送,但是不会跨域,因此一般是将它放入 HTTP 请求的 Header Authorization 字段中。当跨域时,也可以将 JWT 被放置于 POST 请求的数据主体中。
服务器每次收到信息都会对它的前两部分进行加密,然后比对加密后的结果是否跟客户端传送过来的第三部分相同,如果相同则验证通过,否则失败。
一般是在请求头里加入 Authorization,并加上 Bearer 标注:
fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token }})
JWT 本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。
JWT token 破解绕过
空加密算法
JWT 支持使用空加密算法,可以在 header 中指定 alg 为 None
这样的话,只要把 signature 设置为空(即不添加 signature 字段),提交到服务器,任何 token 都可以通过服务器的验证。
修改 RSA 加密算法为 HMAC
JWT 中最常用的两种算法为 HMAC 和 RSA。
- HMAC 是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。
- RSA 则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。
在 HMAC 和 RSA 算法中,都是使用私钥对 signature 字段进行签名,只有拿到了加密时使用的私钥,才有可能伪造 token。
现在我们假设有这样一种情况,一个 Web 应用,在 JWT 传输过程中使用 RSA 算法,密钥 pem
对 JWT token 进行签名,公钥 pub
对签名进行验证。
{ "alg" : "RS256", "typ" : "jwt"}
通常情况下密钥 pem
是无法获取到的,但是公钥 pub
却可以很容易通过某些途径读取到,这时,将 JWT 的加密算法修改为 HMAC,即
{ "alg" : "HS256", "typ" : "jwt"}
同时使用获取到的公钥 pub
作为算法的密钥,对 token 进行签名,发送到服务器端。
服务器端会将 RSA 的公钥(pub
)视为当前算法(HMAC)的密钥,使用 HS256 算法对接收到的签名进行验证。
爆破密钥
JWT 的密钥爆破需要在一定的前提下进行:
- 知悉 JWT 使用的加密算法
- 一段有效的、已签名的 token
- 签名用的密钥不复杂(弱密钥)
所以其实 JWT 密钥爆破的局限性很大。
文件位置/jwtcrack token 例如:root@Jaren:~/c-jwt-cracker# /root/c-jwt-cracker/jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.sh6GvTOAjHLEnmqP_ZoUqWVtddg7KlmHqKPa9VKM5G0
然后就是漫长的等待了。。。
c-jwt-cracker 配置
什么?你还要问 c-jwt-cracker 怎么安装?
- ubuntu 中直接下载:
git clone https://github.com/brendan-rius/c-jwt-cracker.git
- 安装 gcc:
sudo apt install gcc
- 安装 make:
sudo apt install make
- 进入 c-jwt-cracker 目录使用
make
编译:make
- 倘若上一步报错,使用如下命令安装该头文件:
sudo apt-get install libssl-dev
- 正常使用即可:
文件位置/jwtcrack token
JWT 特殊参数的利用
修改 KID 参数
kid
是 jwt header 中的一个可选参数,全称是 key ID
,它用于指定加密算法的密钥
{ "alg" : "HS256", "typ" : "jwt", "kid" : "/home/jwt/.ssh/pem"}
因为该参数可以由用户输入,所以也可能造成一些安全问题。
任意文件读取
kid
参数用于读取密钥文件,但系统并不会验证用户想要读取的到底是不是密钥文件
所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。
{ "alg" : "HS256", "typ" : "jwt", "kid" : "/etc/passwd"}
SQL 注入
kid
也可以从数据库中提取数据,这时候就有可能造成 SQL 注入攻击,通过构造 SQL 语句来获取数据或者是绕过 signature 的验证
{ "alg" : "HS256", "typ" : "jwt", "kid" : "key11111111' || union select 'secretkey' -- "}
命令注入
对 kid
参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。
如果服务器后端使用的是 Ruby
,在读取密钥文件时使用了 open
函数,通过构造参数就可能造成命令注入。
"/path/to/key_file|whoami"
对于其他的语言,例如 php,如果代码中使用的是 exec
或者是 system
来读取密钥文件,那么同样也可以造成命令注入。
修改 JKU/X5U 参数
JKU
的全称是 “JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的 URL。类似于 kid
,JKU
也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定 web 应用使用该组密钥来验证 token。
X5U
则以 URL 的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与 JKU
的攻击利用方式类似。