前言
在目前微服务的架构下,使用JWT作为用户的身份认证方式越来越常见。本篇文章就来认识一下JWT的生成方式以及可能存在的安全风险。
由于本人水平有限,文章中可能会出现一些错误,欢迎各位大佬指正,感激不尽。如果有什么好的想法也欢迎交流~~
JWT简介
JWT(JSON Web Token)是一种用于身份认证和授权的开放标准,它通过在网络应用间传递被加密的JSON数据来安全地传输信息使得身份验证和授权变得更加简单和安全。
JWT作用
用来身份验证和授权
JWT对于渗透测试人员而言可能是一种非常吸引人的攻击途径,因为它们不仅是让你获得无限访问权限的关键而且还被视为隐藏了通往以下特权的途径,例如:特权升级、信息泄露、SQLi、XSS、SSRF、RCE、LFI等。
JWT基本结构
JWT(JSON Web Token)的结构由三部分组成,分别是Header、Payload和Signature,每个部分以“.”分割。如下图:
下面来具体说一下每个部分
Header
Header包含了JWT使用的算法和类型等元数据信息,通常使用JSON对象表示并使用Base64编码,Header中包含两个字段:alg和typ
alg(algorithm):指定了使用的加密算法,常见的有HMAC、RSA和ECDSA等算法
typ(type):指定了JWT的类型,通常为JWT
上面为hearder的一个实例,其中alg指定了使用HMAC-SHA256算法进行签名,typ指定了JWT的类型为JWT
Payload
Payload包含了JWT的主要信息,通常使用JSON对象表示并使用Base64编码,Payload中包含三个类型的字段:注册声明、公共声明和私有声明
这部分是重要的,可以自定义信息保存在此
公共声明(Public Claims):使用 JWT 的人可以随意定义这些声明( 可以自己声明一些有效信息如用户的id,name等,但是不要设置一些敏感信息,如密码 )。但是为了避免冲突,应该在 JWT注册表中定义它们,或者将它们定义为包含抗冲突名称空间的 URI。
私有声明(Private Claims):这些是创建用于在同意使用它们的各方之间共享信息的习惯声明,既不是注册声明,也不是公开声明( 私人声明是提供者和消费者所共同定义的声明 )。
注册声明(Registered Claims):预定义的标准字段,包含了一些JWT的元数据信息,例如:发行者、过期时间等
{
"sub": "1234567890",// 注册声明
"name": "John Doe",// 公共声明
"admin": true // 私有声明
}
这里共同声明和私有声明都是可以自己定义的,因此要额外关注,注意是否存在敏感信息。
另外注册声明是JWT已经预定义好的,通常有下面的一些字段
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
注意:对于已签名的Token,这些信息虽然受到保护,不会被篡改,但任何人都可以阅读。除非加密,否则不要将机密信息放在 JWT 的有效负载或头元素中。
Signature
Signature是使用指定算法对Header和Payload进行签名生成的,用于验证JWT的完整性和真实性,Signature的生成方式通常是将Header和Payload连接起来然后使用指定算法对其进行签名,最终将签名结果与Header和Payload一起组成JWT,Signature的生成和验证需要使用相同的密钥,下面是一个示例Signature
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
其中HMACSHA256是使用HMAC SHA256算法进行签名,header和payload是经过Base64编码的Header和Payload,secret是用于签名和验证的密钥,最终将Header、Payload和Signature连接起来用句点(.)分隔就形成了一个完整的JWT,下面是一个示例JWT,其中第一部分是Header,第二部分是Payload,第三部分是Signature,注意JWT 中的每一部分都是经过Base64编码的,但并不是加密的,因此JWT中的信息是可以被解密的
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
在线平台
下面是一个JWT在线构造和解构的平台:
https://jwt.io/
JWT攻击面
敏感信息泄露
由于Header和Payload部分是使用可逆base64方法编码的,因此任何能够看到令牌的人都可以读取数据。如果在Header和Payload内写了敏感信息,则会导致敏感信息泄露。
可以使用上面的在线工具对JWT进行解密,,存在敏感信息的话就可以直接拿到
签名未校验
JWT(JSON Web Token)的签名验证过程主要包括以下几个步骤:
分离解构:JWT的Header和Payload是通过句点(.)分隔的,因此需要将JWT按照句点分隔符进行分离
验证签名:通过使用指定算法对Header和Payload进行签名生成签名结果,然后将签名结果与JWT中的签名部分进行比较,如果两者相同则说明JWT的签名是有效的,否则说明JWT的签名是无效的
验证信息:如果JWT的签名是有效的则需要对Payload中的信息进行验证,例如:可以验证JWT中的过期时间、发行者等信息是否正确,如果验证失败则说明JWT是无效的
下面是一个使用JAVA进行JWT签名验证的示例代码:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JWTExample {
private static final String SECRET_KEY = "my_secret_key";
public static void main(String[] args) {
// 构建 JWT
String jwtToken = Jwts.builder()
.setSubject("1234567890")
.claim("name", "John Doe")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
// 验证 JWT
try {
// 分离 Header, Payload 和 Signature
String[] jwtParts = jwtToken.split("\.");
String header = jwtParts[0];
String payload = jwtParts[1];
String signature = jwtParts[2];
// 验证签名
String expectedSignature = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(jwtToken)
.getSignature();
if (!signature.equals(expectedSignature)) {
throw new RuntimeException("Invalid JWT signature");
}
// 验证 Payload 中的信息
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(jwtToken)
.getBody();
System.out.println("Valid JWT");
} catch (Exception e) {
System.out.println("Invalid JWT: " + e.getMessage());
}
}
}
在上面的示例代码中使用jwt库进行JWT的签名和验证,首先构建了一个JWT,然后将其分离为Header、Payload和Signature三部分,使用parseClaimsJws函数对JWT进行解析和验证,从而获取其中的Payload中的信息并进行验证,最后如果解析和验证成功,则说明JWT是有效的,否则说明JWT是无效的,在实际应用中应该将SECRET_KEY替换为应用程序的密钥。
如果没有验证签名的一步,我们就可以修改JWT内的内容
空加密算法
JWT支持将算法设定为“None”。如果“alg”字段设为“ None”,那么签名会被置空,这样任何token都是有效的。
设定该功能的最初目的是为了方便调试。但是,若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为“None”来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站。
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicGhvbmUiOiIxMzIxMjM0MTIzNCIsImlhdCI6MTUxNjIzOTAyMn0.tcn0SxFEnRPW8s80RKz30HhZCvTFf5KkArOUoJ6YMiM
设置 “alg”: “none”不带签名,生成Token:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJhY3Rpb24iOiJ1cGxvYWQifQ.
解构:
{"typ":"JWT","alg":"none"}.
{"user":"admin","action":"upload"}.
[ ]
页面是否仍然返回有效?如果页面返回有效,那么说明存在漏洞。
密钥混淆攻击
JWT使用的加密算法有对称加密算法以及非对称加密算法两种,最常用的就是HMAC和RSA。
HMAC(对称加密算法)用同一个密钥对token进行签名和认证。而RSA(非对称加密算法)需要两个密钥,先用私钥加密生成JWT,然后使用其对应的公钥来解密验证。
我们知道JWT中的Header头的内容我们是可以修改的,如果将算法RS256修改为HS256(非对称密码算法=>对称密码算法)会出现什么情况?
有一种情况是后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。由于公钥有时可以被攻击者获取到(公钥一般是公开的),所以攻击者可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。
示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.I3G9aRHfunXlZV2lyJvWkZO0I_A_OiaAAQakU_kjkJM
解构:
{"typ":"JWT","alg":"HS256"}. {"login":"ticarpi"}. [使用HS256签名,使用RSA公钥文件作为密钥验证。]
后端代码会使用RSA公钥+HS256算法进行签名验证。JWT配置应该只允许使用HMAC算法或公钥算法,决不能同时使用这两种算法。
暴力破解密钥
HMAC签名密钥(例如HS256 / HS384 / HS512)使用对称加密,这意味着对令牌进行签名的密钥也用于对其进行验证。由于签名验证是一个自包含的过程,因此可以测试令牌本身的有效密钥,而不必将其发送回应用程序进行验证。
因此,HMAC JWT破解是离线的,通过JWT破解工具,可以快速检查已知的泄漏密码列表或默认密码。
我们可以使用jwt_tool进行破解密钥
工具github地址:https://github.com/ticarpi/jwt_tool
工具详细作用见下面工具集合
使用方式
python jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SodQL2sIh18D4fFGjQvN6xNAUVlXiOiskm-QkEzEYaM -C -d passwd.txt
如果爆破成功,则可以伪造任意用户Token。
密钥泄露
上面说了爆破密钥,目前企业的安全意识都有所提高,一般不会使用弱密码,因此爆破密钥成功的可能性降低。如果通过其它的方式泄露了密钥,则也可能造成跟上面一样的危害,导致可以伪造任意用户的Token。
泄露的可能途径
git信息泄露
目录遍历
任意文件读取
xxe漏洞
......
JWT头部参数注入
根据JWS规范,只有头部参数alg是一定要有的。但是在实际生产中,JWT头部不仅仅有alg参数,往往还包含其它参数。以下三个是实际生活中通常测试攻击的参数:
➤jwk(JSON Web Key):是一种用于表示密钥的嵌入式 JSON 对象。如下面所示:
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
问题:正常时候服务器应该要对其进行限制,只能使用特定的公钥白名单进行签名验证。然而,如果出现配置失误或疏忽,某些服务器可能会误用JWK参数内嵌的任何密钥进行签名验证操作。这就意味着,如果出现jwk配置失误,攻击者就可以通过RSA私钥对已经修改过的payload进行签名,并将RSA公钥嵌入到JWK头部,从而绕过签名认证。
➤ jku(JSON Web Key Set URL):是一种用于提供URL的机制,发送jwk的地址。与"key ID" (kid) 类似,jku 也可以由用户根据特定输入数据来指定。一旦用户的输入数据没有经过严格的过滤和验证,就可能导致潜在的安全漏洞。
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
}
将JKU加入到JKT中,kid需要改成生成公钥的kid:
{
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"alg": "RS256",
"jku": "https://exploit-0a4700ee034ed5dc8174bf6f01c3000c.exploit-server.net/exploit"
}
➤ kid(Key ID):即密钥标识符,被用作提供一个独特的标识,当存在有多个密钥的情况下,服务器就会通过此ID来准确识别应用于验证的正确密钥。但是,系统并不具备辨别用户意图的能力,因此如果缺乏对参数的过滤措施,攻击者就可能篡改密钥文件,从而实现任意文件读取攻击,借此读取系统内的各类文件资源。
kid的用法一般是从文件或者数据库中获取加密的密钥,因此不正确的利用可能导致目录遍历以及SQL注入,在一些特殊的情况下还会造成命令注入。实际遇到这样的案例比较少。
1)目录遍历
由于KID通常用于从文件系统中检索密钥文件,因此,如果在使用前没有正确处理KID,文件系统可能会遭到目录遍历攻击。这样,攻击者便能够在文件系统中指定任意文件作为认证的密钥。
"kid": "../../../../etc/test" //使用公共文件test验证token
例如,攻击者可以强行设定应用程序使用公开可用文件作为密钥,并用该文件给HMAC加密的token签名。
2)SQL注入
KID也可以用于在数据库中检索密钥。在该情况下,攻击者很可能会利用SQL注入来绕过JWT安全机制。
如果可以在KID参数上进行SQL注入,攻击者便能使用该注入返回任意值。
"kid":"aaaaaaa' UNION SELECT 'key';--" //使用字符串"key"验证token
上面这个注入会导致应用程序返回字符串“ key”(因为数据库中不存在名为“ aaaaaaa”的密钥)。然后使用字符串“ key”作为密钥来认证token。
3)命令注入
有时,将KID参数直接传到不安全的文件读取操作可能会让一些命令注入代码流中。
一些函数就能给此类型攻击可乘之机,比如Ruby open()。攻击者只需在输入的KID文件名后面添加命令,即可执行系统命令:
"key_file" | whoami;
类似情况还有很多,这只是其中一个例子。理论上,每当应用程序将未审查的头部文件参数传递给类似system(),exec()的函数时,都会产生此种漏洞。
工具集合
jwt_tool
项目地址 :https://github.com/ticarpi/jwt_tool
功能
1、检查令牌的有效性
2、测试已知漏洞:
CVE-2015-2951:alg=none签名绕过漏洞
CVE-2016-10555:RS / HS256公钥不匹配漏洞
CVE-2018-0114:Key injection漏洞
CVE-2019-20933 / CVE-2020-28637:Blank password漏洞
CVE-2020-28042:Null signature漏洞
3、扫描配置错误或已知漏洞
4、Fuzz声明值以引发意外行为
5、测试secret/key file/public key/ JWKS key的有效性
6、通过高速字典攻击识别低强度key
7、时间戳篡改
8、RSA和ECDSA密钥生成和重建(来自JWKS文件)
9、伪造新的令牌头和有效载荷内容,并使用密钥或通过其他攻击方法创建新的签名
Jwtcrack
Jwtcrack 是一个用于暴力破解 JWT 签名的工具。它会尝试使用不同的算法、密钥和有效载荷来生成签名,并与目标 JWT 进行比较,以验证签名的有效性。它通常用于渗透测试和安全评估,来检测弱 JWT 签名的安全风险。
JWT攻击常见防御措施
1、使用最新的库来处理JWT,并确保开发人员对相关安全问题足够了解。现代代码库的使用降低了在代码实现中引入安全漏洞的可能性,但由于相关规范固有的灵活性,也并非万无一失。
2、确保对收到的任何JWT进行严格的签名验证,并考虑边缘情况,如使用非预期的算法签名的JWT。
3、为jku头部提供允许主机白名单,并严格执行。
4、确保不会受到kid头部参数路径穿越或SQL注入的影响。
5、为发行的任何令牌设置到期日期。
6、尽可能避免在 URL 参数中发送令牌。
7、使发行服务器能够撤销令牌(例如,在注销时)。
原文始发于微信公众号(信安路漫漫):JWT攻击面分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论