JWT是什么
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,可以作为JSON对象在各方之间安全地传输信息。由于该信息是数字签名的,因此可以对其进行验证和信任。
它遵循JSON格式,将用户信息加密到token里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证token,通过token验证用户身份。基于token的身份验证可以替代传统的cookie+session身份验证方法。
什么时候使用JWT
下列场景中使用JSON Web Token是很有用的:
- Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
- Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
JWT的格式
jwt由三个部分组成:header.payload.signature
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 签名
签名用于验证消息是否在整个过程中被更改,对于使用私钥签名的令牌,它还可以验证 JWT 的发送方是否是它所说的那个发送方。
值得注意的是,编码header和payload时使用的编码方式为base64urlencode
,base64url
编码是base64
的修改版,为了方便在网络中传输使用了不同的编码表,它不会在末尾填充”=”号,并将标准Base64中的”+”和”/“分别改成了”-“和”-“。
要创建签名部分,您必须获取已编码的标题、已编码的有效负载、秘钥、标题中指定的算法,并对其进行签名,例如,如果您想使用 HMAC sha256算法,签名将以下列方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名加解密
Go实现一次HS256加解密
package main
import (
"fmt"
"jwt-go-master"
"time"
)
func CreateToken(uid, secret string) (string, error) {
at := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"uid": uid,
"exp": time.Now().Add(time.Minute * 15).Unix(),
})
token, err := at.SignedString([]byte(secret))
if err != nil {
return "", err
}
return token, nil
}
func ParseToken(token string, secret string) (string, error) {
claim, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return "", err
}
return claim.Claims.(jwt.MapClaims)["uid"].(string),err
}
func main(){
str,_ := CreateToken("admin","test")
fmt.Println(str)
str1,_ := ParseToken(str,"test")
fmt.Println(str1)
}
PS D:\GO\GoProject\src\go_code\jwt> go run .\main.go
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDc1MDQ0MjcsInVpZCI6ImFkbWluIn0.ZN-CUZjnfmZJTeGd6vPBj0U06S6HL7jr-S7YJL-1Ljw
admin
攻击JWT认证
JWT签名算法中,一般有两个选择,一个采用HS256(对称加密), 另外一个就是采用RS256(非对称加密)。当然不排除ECDSA(非对称加密)。
https://owasp.org/www-chapter-vancouver/assets/presentations/2020-01_Attacking_and_Securing_JWT.pdf
在对JWT进行攻击利用之前,需要先明确,JWT是一个标准,可以有多种多样的实现。且他只对消息进行签名,即确保这条消息在传输过程中没有被篡改。
CVE-2015-2951 - 空加密算法
https://www.whitesourcesoftware.com/vulnerability-database/CVE-2015-2951
JWT provided by F21 is a PHP library for handling JSON Web Tokens. php-jwt contains a vulnerability where it fails to verify token signatures. F21提供的 JWT 是一个用于处理 JSON Web 令牌的 PHP 库。Php-jwt 包含一个漏洞,它无法验证令牌签名
该编号年代久远 https://github.com/F21/jwt
修复:https://github.com/F21/jwt/commit/a327cf9052df8f9f97728ca0b5fa78a8231b79b6#diff-f07c9fd15d843b0265165cdeefa9f78cf1270102afd06eba75a0bc3aa8ab0734R54
但是引出来一个种类的风险,即 alg = none
JWT支持使用空加密算法,这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。举个例子,使用以下的字段
{
"alg" : "None",
"typ" : "jwt"
}
{
"user" : "Admin"
}
生成的完整token为ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0
(header+'.'+payload,去掉了'.'+signature字段)
空加密算法的设计初衷是用于调试的,但是如果某天开发人员脑阔瓦特了,在生产环境中开启了空加密算法,缺少签名算法,jwt保证信息不被篡改的功能就失效了。攻击者只需要把alg字段设置为None,就可以在payload中构造身份信息,伪造用户身份。
CVE-2016-10555 - 修改RSA加密算法为HMAC
https://blog.pentesteracademy.com/hacking-jwt-tokens-verification-key-mismanagement-1b69c89ffdfb
Since “algorithm” isn’t enforced in jwt.decode()in jwt-simple 0.3.0 and earlier, a malicious user could choose what algorithm is sent sent to the server. If the server is expecting RSA but is sent HMAC-SHA with RSA’s public key, the server will think the public key is actually an HMAC private key. This could be used to forge any data an attacker wants. 由于 jwt-simple 0.3.0和更早版本的 jwt.decode ()中不执行“ algorithm”,恶意用户可以选择发送到服务器的算法。如果服务器期望得到 RSA,但是使用 RSA 的公钥发送 HMAC-SHA,那么服务器将认为公钥实际上是 HMAC 私钥。这可以用来伪造攻击者想要的任何数据
该编号针对于 node.js 的 jwt-simple项目。
issue: https://github.com/hokaccha/node-jwt-simple/pull/16
同样引出来一类型的风险,即将RSA 替换为 HMAC 并使用公钥作为秘钥,导致可以被伪造任意身份。
JWT中最常用的两种算法为HMAC
和RSA
。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算法对接收到的签名进行验证。
一个案例:https://skysec.top/2018/05/19/2018CUMTCTF-Final-Web/#Pastebin/
CVE-2018-0114 - kid 注入
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0114
A vulnerability in the Cisco node-jose open source library before 0.11.0 could allow an unauthenticated, remote attacker to re-sign tokens using a key that is embedded within the token. The vulnerability is due to node-jose following the JSON Web Signature (JWS) standard for JSON Web Tokens (JWTs). This standard specifies that a JSON Web Key (JWK) representing a public key can be embedded within the header of a JWS. This public key is then trusted for verification. An attacker could exploit this by forging valid JWS objects by removing the original signature, adding a new public key to the header, and then signing the object using the (attacker-owned) private key associated with the public key embedded in that JWS header. 0.11.0之前 Cisco node-jose 开放源码库中的一个漏洞可能允许未经身份验证的远程攻击者使用嵌入在令牌中的密钥重新签署令牌。该漏洞是由于遵循 JSON Web 令牌(JWTs)的 JSON Web 签名(JWS)标准的 node-jose 造成的。该标准规定可以将表示公钥的 JSON Web Key (JWK)嵌入到 JWS 的头中。然后信任此公钥以进行验证。攻击者可以利用这一点伪造有效的 JWS 对象,方法是删除原始签名,向标头添加一个新的公钥,然后使用与嵌入在 JWS 标头中的公钥相关联的(攻击者拥有的)私钥对对象进行签名
JWS标准:https://datatracker.ietf.org/doc/html/rfc7515
该编号漏洞案例:https://blog.pentesteracademy.com/hacking-jwt-tokens-jws-standard-for-jwt-666810809323
这个漏洞引发了 kid 相关的攻击,首先看看RFC7515对于KID的描述
4.1.4. “kid” (Key ID) Header Parameter The “kid” (key ID) Header Parameter is a hint indicating which key was used to secure the JWS. This parameter allows originators to explicitly signal a change of key to recipients. The structure of the “kid” value is unspecified. Its value MUST be a case-sensitive string. Use of this Header Parameter is OPTIONAL. When used with a JWK, the “kid” value is used to match a JWK “kid” parameter value. “ kid”(密钥 ID) Header 参数是一个提示,指示使用哪个密钥来保护 JWS。此参数允许发起者向收件人显式发出密钥更改的信号。“ kid”值的结构未指定。它的值必须是区分大小写的字符串。此 Header 参数的使用是可选的。当与 JWK 一起使用时,“ kid”值用于匹配 JWK“ kid”参数值
任意文件读取
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
来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。
CVE-2019-20933/CVE-2020-28637 空秘钥
https://github.com/influxdata/influxdb/commit/761b557315ff9c1642cf3b0e5797cd3d983a24c0
InfluxDB before 1.7.6 has an authentication bypass vulnerability in the authenticate function in services/httpd/handler.go because a JWT token may have an empty SharedSecret (aka shared secret). 由于/handler.go 中没有对空秘钥进行检测,所以可以用空秘钥来进行认证
项目的修复代码。
这里则是一类新的风险,是否存在空秘钥
CVE-2020-28042 空签名
https://www.shielder.com/blog/2020/11/re-discovering-a-jwt-authentication-bypass-in-servicestack/
漏洞发现者对于漏洞的描述 Every time I see JWT tokens, I have kind of a routine of tests I do to check for common JWT libraries vulnerabilities.
At some point, during the test, I tried to remove the signature, without changing the header and with my big surprise the authenticated API I was testing answered with a “200 OK”. 每当我看到JWT令牌时,我都会进行某种测试例程,以检查常见的JWT库漏洞。
在测试期间的某一时刻,我试图删除签名,但没有改变头部,令我吃惊的是,我测试的经过身份验证的API回答了“200 OK”。
可以看到这个问题本身是出在JWT没有对不含签名的请求进行验证,导致出现了绕过。
该问题出现在ServiceStack组件上。
https://github.com/ServiceStack/ServiceStack/commit/540d4060e877a03ae95343c1a8560a26768585ee?diff=split
可以在这里引出JWT的一个新风险,将秘钥置空查看后端是否校验
爆破
只支持对称加密爆破(HMAC)。
搜集了三款工具
jwt_tool -> python
项目地址:
https://github.com/ticarpi/jwt_tool
jwt-hack -> Go
项目地址:
https://github.com/hahwul/jwt-hack
个人推荐使用这个,速度快
c-jwt-cracker ->C
项目地址:
https://github.com/brendan-rius/c-jwt-cracker
这个之前使用过几次,想过不太理想,可能是我没深入研究这个工具,感兴趣的朋友可以自己学习
总结
首先说明了JWT是什么,然后对不同的JWT实现产生的风险进行了总结,每次测试JWT都能对照这些风险点进行测试。
风险checklist:
- 空加密算法
- 修改RSA加密算法为HMAC
- kid 注入
- 空秘钥
- 空签名
- 爆破
主要参考
https://xz.aliyun.com/t/6776#toc-5%E3%80%81
https://coolshell.cn/articles/19395.html
https://t.zsxq.com/bURR7uF