JWT 基础概念详解

简介

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则

并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。

JWT组成

JWT安全问题-LMLPHP

JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:

  • Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
  • Payload : 用来存放实际需要传递的数据
  • Signature(签名) :服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

JWT 通常是这样的:xxxxx.yyyyy.zzzzz

示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你可以在 (https://jwt.io/) 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。

Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。

JWT安全问题-LMLPHP

Header

Header 通常由两部分组成:

  • typ(Type):令牌类型,也就是 JWT。
  • alg(Algorithm) :签名算法,比如 HS256。

示例:

{
  "alg": "HS256",
  "typ": "JWT"
}

JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。

Payload

Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。

Claims 分为三种类型:

  • Registered Claims(注册声明) :预定义的一些声明,建议使用,但不是强制性的。
  • Public Claims(公有声明) :JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 JSON Web Token Registryopen w中定义它们。
  • Private Claims(私有声明) :JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。

下面是一些常见的注册声明:

  • iss(issuer):JWT 签发方。
  • iat(issued at time):JWT 签发时间。
  • sub(subject):JWT 主题。
  • aud(audience):JWT 接收方。
  • exp(expiration time):JWT 的过期时间。
  • nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
  • jti(JWT ID):JWT 唯一标识。

示例:

{
  "uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
  "sub": "1234567890",
  "name": "John Doe",
  "exp": 15323232,
  "iat": 1516239022,
  "scope": ["admin", "user"]
}

Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分

Signature

Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。

这个签名的生成需要用到:

  • Header + Payload。
  • 存放在服务端的密钥(一定不要泄露出去)。
  • 签名算法。

签名的计算公式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,这个字符串就是 JWT

如何基于 JWT 进行身份验证?

在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。

JWT安全问题-LMLPHP

简化后的步骤如下:

  1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。
  2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
  3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
  4. 服务端检查 JWT 并从中获取用户相关信息。

两点建议:

  1. 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
  2. 请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 Authorization 字段中(Authorization: Bearer Token

如何防止 JWT 被篡改?

有了签名之后,即使 JWT 被泄露或者解惑,黑客也没办法同时篡改 Signature 、Header 、Payload

这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。

不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature 、Header 、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。

密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥

如何加强 JWT 的安全性?

  1. 使用安全系数高的加密算法。
  2. 使用成熟的开源库,没必要造轮子。
  3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。
  4. 一定不要将隐私信息存放在 Payload 当中。
  5. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
  6. Payload 要加入 exp (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。

JWT攻击

敏感信息泄露

JWT保证的是数据传输过程中的完整性而不是机密性。

由于payload是使用base64url编码的,所以相当于明文传输,如果在payload中携带了敏感信息(如存放密钥对的文件路径),单独对payload部分进行base64url解码,就可以读取到payload中携带的信息。

加密算法

空加密算法

JWT支持使用空加密算法,可以在header中指定alg为None

这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。举个例子,使用以下的字段

{
    "alg" : "None",
    "typ" : "jwt"
}

{
    "user" : "Admin"
}

生成的完整token为ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0

(header+'.'+payload,去掉了'.'+signature字段)

空加密算法的设计初衷是用于调试的,但是如果某天开发人员脑阔瓦特了,在生产环境中开启了空加密算法,缺少签名算法,jwt保证信息不被篡改的功能就失效了。攻击者只需要把alg字段设置为None,就可以在payload中构造身份信息,伪造用户身份

修改RSA加密算法为HMAC

JWT中最常用的两种算法为HMACRSA

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算法对接收到的签名进行验证。

配置应该只允许使用HMAC算法或公钥算法,决不能同时使用这两种算法。

爆破密钥

俗话说,有密码验证的地方,就有会爆破。

不过对 JWT 的密钥爆破需要在一定的前提下进行:

  • 知悉JWT使用的加密算法
  • 一段有效的、已签名的token
  • 签名用的密钥不复杂(弱密钥)

所以其实JWT 密钥爆破的局限性很大

PyJWT库具体地址为:https://github.com/jpadilla/pyjwt

修改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。类似于kidJKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。

X5U则以URI的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU的攻击利用方式类似。

4、无效签名
当用户端提交请求给应用程序,服务端可能没有对token签名进行校验,这样,攻击者便可以通过提供无效签名简单地绕过安全机制。
示例:
一个很好的例子是网站上的“个人资料”页面,因为我们只有在被授权通过有效的JWT进行访问时才能访问此页面,我们将重放请求并寻找响应的变化以发现问题

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCIsImFjdGlvbiI6InByb2ZpbGUifQ.FjnAvQxzRKcahlw2EPd9o7teqX-fQSt7MZhT84hj7mU

user 字段改为 admin,重新生成新 token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJhY3Rpb24iOiJwcm9maWxlIn0._LRRXAfXtnagdyB1uRk-7CfkK1RESGwxqQCdwCNSPaI

结构:

{"typ": "JWT", "alg": "HS256"}.
{"user": "admin","action": "profile"}.
[新的签名]

将重新生成的Token发给服务端效验,如访问页面正常,则说明漏洞存在

5. 破解HS256(对称加密算法)密钥

如果HS256密钥的强度较弱的话,攻击者可以直接通过蛮力攻击方式来破解密钥,例如将密钥字符串用作PyJWT库示例代码中的密钥的时候情况就是如此。

然后,用蛮力方式对密钥进行猜解,具体方法很简单:如果密钥正确的话,解密就会成功;如果密钥错误的话,解密代码就会抛出异常。

此外,我们也可以使用PyJWT或John Ripper进行破解测试。

PyJWT库具体地址为:https://github.com/jpadilla/pyjwt

JWT tool

此工具可用于测试jwt的安全性,地址是 https://github.com/ticarpi/jwt_tool
示例用法:

λ python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn 9dm15Po

   $$$$$\ $$\      $$\ $$$$$$$$\  $$$$$$$$\                  $$\
   \__$$ |$$ | $\  $$ |\__$$  __| \__$$  __|                 $$ |
      $$ |$$ |$$$\ $$ |   $$ |       $$ | $$$$$$\   $$$$$$\  $$ |
      $$ |$$ $$ $$\$$ |   $$ |       $$ |$$  __$$\ $$  __$$\ $$ |
$$\   $$ |$$$$  _$$$$ |   $$ |       $$ |$$ /  $$ |$$ /  $$ |$$ |
$$ |  $$ |$$$  / \$$$ |   $$ |       $$ |$$ |  $$ |$$ |  $$ |$$ |
\$$$$$$  |$$  /   \$$ |   $$ |       $$ |\$$$$$$  |\$$$$$$  |$$ |
 \______/ \__/     \__|   \__|$$$$$$\__| \______/  \______/ \__|
  Version 1.3                 \______|


=====================
Decoded Token Values:
=====================

Token header values:
[+] typ = JWT
[+] alg = HS256

Token payload values:
[+] login = ticarpi

----------------------
JWT common timestamps:
iat = IssuedAt
exp = Expires
nbf = NotBefore
----------------------


########################################################
#  Options:                                            #
#                ==== TAMPERING ====                   #
#  1: Tamper with JWT data (multiple signing options)  #
#                                                      #
#             ==== VULNERABILITIES ====                #
#  2: Check for the "none" algorithm vulnerability     #
#  3: Check for HS/RSA key confusion vulnerability     #
#  4: Check for JWKS key injection vulnerability       #
#                                                      #
#            ==== CRACKING/GUESSING ====               #
#  5: Check HS signature against a key (password)      #
#  6: Check HS signature against key file              #
#  7: Crack signature with supplied dictionary file    #
#                                                      #
#            ==== RSA KEY FUNCTIONS ====               #
#  8: Verify RSA signature against a Public Key        #
#                                                      #
#  0: Quit                                             #
########################################################

Please make a selection (1-6)
> 1

其中的选项分别为:

1. 修改JWT
2. 生成None算法的JWT
3. 检查RS/HS256公钥错误匹配漏洞
4. 检测JKU密钥是否可伪造
5. 输入一个key,检查是否正确
6. 输入一个存放key的文本,检查是否正确
7. 输入字典文本,爆破
8. 输入RSA公钥,检查是否正确

安全建议

一般保证前两点基本就没什么漏洞了。

  • 保证密钥的保密性
  • 签名算法固定在后端,不以JWT里的算法为标准
  • 避免敏感信息保存在JWT中
  • 尽量JWT的有效时间足够短
11-16 09:43