JWT认证安全分析

前言

0x01 JWT相关

1. JWT定义

JWT 是 JSON web token ,为了在网络应用环境中传递声明而执行的一种基于json的开放标准。非常适用于分布式的单点登录场景。主要是用来校验身份提供者和服务提供者之间传递的用户身份信息。

2. JWT结构

jwt由三个部分组成:header.payload.signature,各部分用“.”相连构成一个完整的Token,形如xxxxx.yyyyy.zzzzz

Header部分

header部分最常用的两个字段是alg和typ,alg指定了token加密使用的算法(最常用的为HMAC和RSA算法),typ声明类型为JWT

header:
{
  "alg" : "HS256",
  "typ" : "jwt"
}

Payload部分

payload为用户数据以及一些元数据有关的声明,用以声明权限,比如一次登录的过程可能会传递以下数据

{
  "user_role" : "finn",  //当前登录用户
  "iss": "admin",     //该JWT的签发者
  "iat": 1573440582,    //签发时间
  "exp": 1573940267,    //过期时间
  "nbf": 1573440582,     //该时间之前不接收处理该Token
  "domain": "example.com",  //面向的用户
  "jti": "dff4214121e83057655e10bd9751d657"  //Token唯一标识
}

Signature部分

signature的功能是保护token完整性。生成方法为将header和payload两个部分联结起来,然后通过header部分指定的算法,计算出签名。抽象成公式就是

signature = HMAC-SHA256(base64urlEncode(header) + '.' + base64urlEncode(payload), secret_key)

值得注意的是,编码header和payload时使用的编码方式为base64urlencode,base64url编码是base64的修改版,为了方便在网络中传输使用了不同的编码表,它不会在末尾填充"="号,并将标准Base64中的"+"和"/"分别改成了"-"和"_"。

3. JWT完整token生成

一个完整的jwt格式为(header.payload.signature)其中header、payload使用base64url编码,signature通过指定算法生成。

python的Pyjwt使用示例如下:

import jwt
encoded_jwt = jwt.encode({'user_name': 'admin'}, 'key', algorithm='HS256')
print(encoded_jwt)
print(jwt.decode(encoded_jwt, 'key', algorithms=['HS256']))

生成的token为:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiJ9.oL5szC7mFoJ_7FI9UVMcKfmisqr6Qlo1dusps5wOUlo

4. JWT令牌解码

由于Header和Payload部分是使用可逆base64方法编码的,因此任何能够看到令牌的人都可以读取数据。

要读取内容,只需要将每个部分传递给base64解码函数。当然也可以使用一些在线解密网站

5.加密算法

JWT中最常用的两种算法为HMAC和RSA。

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。

RSA则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。

0x02 JWT安全问题分析

1.加密算法

1.1 空加密算法

原理

JWT支持使用空加密算法,可以在header中指定alg为None。这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。

实例

通过普通用户登录,点击投票按钮,显示不是管理员用户:

image-20220216110044412

image-20220216110108147

然后对cookie中的token进行解码查看:

image-20220216110221500

然后将加密算法改为none,将admin值改为true重新加密。这里采用的加密只是base64url加密,可以用脚本生成:

import base64
def b64urlencode(data):
    return base64.b64encode(data).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'')
print(b64urlencode(b'{"alg":"none"}') + b'.' + b64urlencode(b'{"iat":1640403035,"admin":"true","user":"Jerry"}') + b'.')

生成:

eyJhbGciOiJub25lIn0.eyJpYXQiOjE2NDA0MDMwMzUsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJKZXJyeSJ9.

替换之后,作为管理员可以投票

image-20220216152130982

image-20220216152208333

1.2 修改加密算法

原理

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

实例

访问:

https://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php

image-20220216154650153

得到jwttoken信息,然后解密查看:

image-20220216154745886

加密算法为rs256。访问http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem得到公钥:

image-20220216155009221

然后通过py重新加密生成

import jwt
public = open('public.pem', 'r').read()
print (public)
print (jwt.encode({"data":"test"}, key=public, algorithm='HS256')) 

在python安装jwt之后运行encode可能会遇到:

image-20220216163259240

解决方式为找到python中jwt包中的algorithms.py文件,然后注释下面几行

image-20220216163342061

最后利用生成的token可以伪造信息

image-20220216163427222

2. KID参数

2.1 任意文件读取

原理

KID参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。

利用

{
    "alg" : "HS256",
    "typ" : "jwt",
    "kid" : "/etc/passwd"
}

通过在kid参数中添加相应绝对或者相对路径进行读取敏感文件信息。

2.2 sql注入

原理

kid也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证

实例

Webgoat的最后一关,当前是jerry的用户,需要删除tom的用户

image-20220216170531806

image-20220216170646566

image-20220216170746504

这道题是利用sql注入解题,其中kid参数的webgoat_key就是jwt加密的secret,我们的目的就是利用kid的注入,让secret可控,进而可以生成合法token

首先kid参数可以变成:a' union select 'VG9t' from information_schema.tables; --

这样sql在回显的时候就是显示联合查询中的内容,然后代码中会进行一次base64解码,所以最后通过这个kid查出来的secret就是可控的,再利用这个密钥重新生成twt即可:

image-20220216173541893

image-20220216173551840

2.3 命令注入

原理

对kid参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open函数,通过构造参数就可能造成命令注入。

利用

"/path/to/key_file|whoami"

3. JKU/X5U参数

原理

JKU的全称是"JSON Web Key Set URL",用于指定一组用于验证令牌的密钥的URL。类似于kid,JKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。

利用

{
  "typ": "JWT",
  "jku": "http://xxx.com:8000/jwks.json",
  "alg": "HS256"
}

4. 密钥可爆破

原理

使用弱密钥进行加密,可利用工具进行爆破。

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

a) 知悉JWT使用的加密算法

b) 一段有效的、已签名的token

c) 签名用的密钥不复杂(弱密钥)

实例

拿到一个token

image-20220216214554164

这道题token已过期,修改时间戳后,还需要改变用户名,再用secret重新生成才可以,所以先利用工具爆破密钥。

根据代码,是在数组中随机选择一个作为密钥:

image-20220216215629098

把5个secret作为字典跑一下:

image-20220216215713366

跑出key为:washington

然后修改字段后重新生成token:

image-20220216215814957

image-20220216215833954

参考

https://jwt.io/

https://www.cnblogs.com/Secde0/p/13968608.html

https://xz.aliyun.com/t/6776

https://www.codetd.com/article/7173266

http://wjlshare.com/archives/1403

posted @ 2022-02-16 22:31  N0r4h  阅读(216)  评论(0编辑  收藏  举报