freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

从JWT源码审计来看NONE算法漏洞(CVE-2015-9235)
2021-11-01 23:51:47

研究JWT漏洞时,发现文章并不多,而且大多数都是黑盒测试,遂出现了本文,大佬们勿喷。

JWT简介

1、什么是JWT?

JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用secret(HMAC算法)或使用“RSA或ECDSA的公用/私有key pair密钥对”对JWT进行签名。

尽管可以对JWT进行加密以提供双方之间的secrecy保密性,但我们将重点关注signed tokens已签名的令牌。signed tokens已签名的令牌可以验证其中包含的claims声明的integrity完整性,而encrypted tokens加密的令牌则将这些other parties其他方的claims声明隐藏。当使用“公钥/私钥对”对令牌进行签名时,signature also certifies签名还证明只有持有私钥的一方才是对其进行签名的一方。

摘自官网:

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

2、JWT能做什么?

1、授权

这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

2、信息交换

JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以可以确保发件人是本人。此外,由于签名是使用标头和有效负载计算的,因此还可以验证内容是否遭到篡改。

3、基于session认证所显露的问题

1、开销

每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

2、扩展性

用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求必须还要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力,这也意味着限制了应用的扩展能力。

3、CSRF

因为是基于cookie来进行用户识别的,所以cookie如果被截获,用户就会很容易受到CSRF的攻击。

4、JWT的认证流程

首先,前端通过web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

后端核对用户名和密码成功后,形成一个JWT Token。

后端将JWT字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage中,退出登录时前端删除保存的JWT即可。

前端在每次请求时将JWT放入HTTP Header中的Authorization字段。

后端校验前端传来的JWT的有效性。

验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

5、JWT的结构

5.1、令牌组成:header.payload.signature

1、标头(Header)

2、有效载荷(Payload)

3、签名(Signature)

5.2、Header

标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256(默认,HS256)或RSA(RS256)。它会使用Base64编码组成JWT结构的第一部分。

注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子的,它并不是一种加密过程。

类似这样:

{
"alg": "HS256",  // 加密算法
"typ": "JWT"  // 类型
}

5.3、Payload

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分

标准中注册的声明(建议但是不强制使用):

1、iss:jwt签发者

2、sub:jwt所面向的用户

3、aud:接收jwt的一方

4、exp:jwt的过期时间,这个过期时间必须要大于签发时间

5、nbf:定义在什么时间之前,该jwt都是不可用的

6、iat:jwt的签发时间

7、jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

类似这样:

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

5.4、Signature

前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的Header和Payload以及我们提供的一个密钥,然后使用Header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过

如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), 'secret');

测试环境

在https://jwt.io/网站中收录有各类语言的JWT库实现(有关JWT详细介绍请访问https://jwt.io/introduction/),分别是:

Auth0实现的java-jwt:“maven: com.auth0 / java-jwt / 3.3.0”

Brian Campbell实现的jose4j:“maven: org.bitbucket.b_c / jose4j / 0.6.3”

connect2id实现的nimbus-jose-jwt:“maven: com.nimbusds / nimbus-jose-jwt / 5.7”

Les Haziewood实现的jjwt:“maven: io.jsonwebtoken / jjwt-root / 0.11.1”

Inversoft实现的prime-jwt:“maven: io.fusionauth / fusionauth-jwt / 3.5.0”

Vertx实现的vertx-auth-jwt:“maven: io.vertx / vertx-auth-jwt / 3.5.1”

1635775457_617ff3e11043a87944965.png!small?1635775457292

本文只做简略介绍,每种JWT库的具体实现不同,各自也有优缺点。有兴趣的同学可以研究下,这里贴上一位大佬的测试环境,这些全部囊括其中:

https://github.com/monkeyk/MyOIDC/

黑盒测试

为了方便,这里直接用WebGoat靶场来做测试

直接利用WebGoat的Java源码来启动靶场,是比较麻烦的,因为对jdk的版本要求比较高。

利用docker来搭建WebGoat,依次输入命令:

docker search webgoat
docker pull webgoat/webgoat-8.0:v8.1.0
docker pull webgoat/webwolf:v8.1.0
docker pull webgoat/goatandwolf:v8.1.0
docker images
docker run -d -p 8888:8888 -p 8080:8080 -p 9090:9090 webgoat/goatandwolf:v8.1.0

启动后,访问:

http://192.168.189.128:8080/WebGoat/start.mvc#lesson/JWT.lesson/3

1635776497_617ff7f1cc7f3e3667d7d.png!small?1635776498103

就是这个投票功能,切换用户得到token:

1635776671_617ff89f74ea1d6c2ae79.png!small?1635776671596

点击回收站图标重置投票,提示

Not a valid JWT token, please try again

1635776862_617ff95ed315de6530036.png!small?1635776862952

对应数据包:

1635776883_617ff9732074619b7676e.png!small?1635776883359

可知,只有管理员才可以重置投票

修改token中的前两部分(“.”号分割),分别进行Base64解码:

“alg”的值改为NONE,“admin”的值改为true

1635776979_617ff9d3374e657ecf8dd.png!small?1635776979306

1635776987_617ff9dba0b45497e691b.png!small?1635776987774

拼接修改后的两段Base64编码后,重新发包:

1635777076_617ffa34c83f7d2493bfc.png!small?1635777076962

报错了,去除“=”号:

1635777100_617ffa4cbdf77deb296c6.png!small?1635777101046

还是报错,再把第三段直接删掉,注意保留“.”号:

1635777153_617ffa8124f7846c13fdb.png!small?1635777153225

可成功重置投票。

代码审计

网上大多数文章都是只描述了黑盒测试的步骤,少有此漏洞的代码层面的讲解,接下来利用调试,来深入了解下此漏洞的原理。

先来看WebGoat靶场中,此漏洞的代码片段:

生成access_token,对应的接口为/JWT/votings/login

1635777677_617ffc8db7e4a5707264e.png!small?1635777677983

校验access_token,对应的接口为/JWT/votings

1635777743_617ffccf851722547c17c.png!small?1635777743925

这里用到的JWT库,为上边提到的jjwt,根据pom文件来查看依赖:

<!-- jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
<scope>test</scope>
</dependency>

我们这里直接利用SpringBoot来搭建一个简易的测试环境,方便调试。

具体代码:

package com.example.demo;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.TextCodec;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;

@RestController
public class test {
public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory");
private static String validUsers = "zzz";

@GetMapping("/login")
public void login(@RequestParam("user") String user, HttpServletResponse response) {
if (validUsers.contains(user)) {
Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10))));
claims.put("user", user);
String token = Jwts.builder()
.setClaims(claims)
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD)
.compact();
Cookie cookie = new Cookie("access_token", token);
response.addCookie(cookie);
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
} else {
Cookie cookie = new Cookie("access_token", "");
response.addCookie(cookie);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
}

@GetMapping("/verify")
@ResponseBody
public String getVotes(@CookieValue(value = "access_token", required = false) String accessToken) {
if (StringUtils.isEmpty(accessToken)) {
return "no login";
} else {
try {
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
Claims claims = (Claims) jwt.getBody();
String user = (String) claims.get("user");
if ("zzz".equals(user)) {
return "zzz";
}
if ("admin".equals(user)) {
return "admin";
}
} catch (Exception e) {
return e.toString();
}
}
return "login";
}
}

先正常请求,生成access_token:

访问

http://127.0.0.1:8080/login?user=zzz

获取access_token

再访问

http://127.0.0.1:8080/verify

断点位置在验签解析处:

Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);

1635778001_617ffdd1ac9ec737d8b25.png!small?1635778001858

跟进Jwts.parser()

1635778044_617ffdfc23bf10fdfc412.png!small?1635778044343

来看看DefaultJwtParser的构造方法:

public DefaultJwtParser() {

// 来看官方对于clock的阐述:

// https://github.com/jwtk/jjwt#jws-read-clock-custom
// Custom Clock Support
// If the above setAllowedClockSkewSeconds isn't sufficient for your needs, the timestamps created during parsing for timestamp comparisons can be obtained via a custom time source. Call the JwtParserBuilder's setClock method with an implementation of the io.jsonwebtoken.Clock interface.

For example:
// 如果上述设置允许的时钟倾斜秒不足以满足您的需要,则可以通过自定义时间源获得自定义时间戳。使用io.jsonwebtoken.Clock接口的实现调用JwtParserBuilder's setClock方法。例如:

// Clock clock = new MyClock();
// Jwts.parserBuilder().setClock(myClock)
this.clock = DefaultClock.INSTANCE;
this.allowedClockSkewMillis = 0L;
}

1635778100_617ffe349f728eeece117.png!small?1635778100822

回到

Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);

1635778120_617ffe48e5f77aac2726b.png!small?1635778121026

这个JWT_PASSWORD在上方的定义:

public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory");

接着跟进

\io\jsonwebtoken\impl\DefaultJwtParser.class#setSigningKey()

1635778248_617ffec8d9e6e74f7b8c2.png!small?1635778248945

这个 Assert.hasText() 只是校验了下是否为String:

1635778274_617ffee2ce638cbd63d05.png!small?1635778274933

接着这行:

this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes);

1635778311_617fff077dd30bcda1db7.png!small?1635778311764

这就是为什么刚才要将Key进行Base64编码

给到DefaultJwtParser.keyBytes:

1635778355_617fff330f6f4076b4a20.png!small?1635778355395

然后返回这个DefaultJwtParser对象:

1635778375_617fff47d3f68fa8f3ff8.png!small?1635778376059

回到:

1635778388_617fff546a2f793edc848.png!small?1635778388610

继续跟进DefaultJwtParser#parse方法,首先判断String字符串:

1635778413_617fff6d092cfa079a0eb.png!small?1635778413306

然后初始化Header、Payload和Digest(摘要):

1635778430_617fff7e9c5d56b01f129.png!small?1635778430696

接着就是分隔符个数delimiterCount:

1635778504_617fffc8207f78f274d51.png!small?1635778504200

接着下面的for循环,会将验签的整段token转为char数组:

1635778569_6180000951d074821645b.png!small?1635778569632

var7为token的char数组,var8为此数组中的字符个数。

接着看下这段for循环:

for(int var9 = 0; var9 < var8; ++var9) {
char c = var7[var9];
// 以“.”号来分割
if (c == '.') {

// 先保存分割的这段字符

CharSequence tokenSeq = Strings.clean(sb);

// token分别为前段:

"eyJhbGciOiJIUzUxMiJ9"、"eyJpYXQiOjE2MzY1NTIxODMsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoienp6In0"
String token = tokenSeq != null ? tokenSeq.toString() : null;

// 根据delimiterCount来判断是Header还是Payload,存到对应的field

if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}

// 每次遇到“.”号都将delimiterCount加一,然后清空StringBuilder对象

++delimiterCount;
sb.setLength(0);
} else {

// 将此char字符放入StringBuilder对象
// 结束此for循环时,StringBuilder对象存放着第三段:

"pntCuTlybllQYsg4BHtgNEQrEmheFalhhv6VEU_CFZ18MP8uvVBCLYK0RjAkIZpyF7KLlBhYzdhN20i8zdMU3A"
sb.append(c);
}
}

接着往下:

1635778618_6180003af258af5c130be.png!small?1635778619150

如果分隔符数量不是2,则JWT格式有误,抛出异常。

接着,将刚才筛选出来的第三段给到Digest摘要:

1635778662_61800066cf381e9491fc0.png!small?1635778662937

接着来看这个if判断:

// 如果base64UrlEncodedHeader不为null
if (base64UrlEncodedHeader != null) {
// Base64解码base64UrlEncodedHeader
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);

// 读取Header的内容,给到Map键值对

Map<String, Object> m = this.readValue(payload);

// 这里是关键分支,根据base64UrlEncodedDigest是否为空,不同走向

if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}

1635778703_6180008fc5d79c4a659a2.png!small?1635778704021

可以看到,默认的“alg”为HS512。

现在,更换成POC试下:

access_token=eyJhbGciOiJub25lIn0.eyJpYXQiOjE2MzY1NTIxODMsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiYWRtaW4ifQ.

1635778749_618000bdcecd18a912f51.png!small?1635778749991

对应修改的前两段Base64编码:

“alg”改为了NONE:

1635778772_618000d4aaf76736771c9.png!small?1635778772777

“user”改为了admin:

1635778783_618000df36a0a5fbb09ac.png!small?1635778783410

再根据断点,快速回到我们刚才的位置:

1635778846_6180011e5bada18f0fb3b.png!small?1635778846653

由于这个if判断:

// 如果base64UrlEncodedHeader不为null
if (base64UrlEncodedHeader != null) {
// Base64解码base64UrlEncodedHeader
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);

// 读取Header的内容,给到Map键值对

Map<String, Object> m = this.readValue(payload);

// 这里是关键分支,根据base64UrlEncodedDigest是否为空,不同走向

if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}

我们已经将第三段删除掉了,base64UrlEncodedDigest为null,所以会走到else分支:

header = new DefaultHeader(m);

来看DefaultHeader的构造方法:

\io\jsonwebtoken\impl\DefaultHeader.class
public DefaultHeader(Map<String, Object> map) {
super(map);
}

再来看super:

\io\jsonwebtoken\impl\JwtMap.class
public JwtMap(Map<String, Object> map) {
Assert.notNull(map, "Map argument cannot be null.");
this.map = map;
}

所以,实例化的DefaultHeader对象给到header:

1635779105_618002212c7206365fef7.png!small?1635779105590

接着往下:

1635779118_6180022ebbba4ca30b37b.png!small?1635779118808

跟进

\io\jsonwebtoken\impl\compression\DefaultCompressionCodecResolver.class#resolveCompressionCodec()

1635779139_6180024303d5228550c31.png!small?1635779139291

接着跟进此类的getAlgorithmFromHeader方法:

1635779153_618002513c87044b9ee6c.png!small?1635779153374

分别来看这两行:

Assert.notNull(header, "header cannot be null.");
return header.getCompressionAlgorithm();

先来看Assert.notNull(header, "header cannot be null.");

Assert,断言

就是断定某一个实际的值是否为自己预期想得到的,如果不一样就抛出异常。

这里的断言,是jjwt库自实现的,跟进下这个notNull方法:

\io\jsonwebtoken\lang\Assert.class#notNull()

1635779189_618002750a2737f35c04b.png!small?1635779189296

判断传入的Object对象是否为null。

再来看return header.getCompressionAlgorithm();

先来执行下:

1635779304_618002e87363a483232c5.png!small?1635779304673

返回null

具体跟进看下

\io\jsonwebtoken\impl\DefaultHeader.class#getCompressionAlgorithm()

1635779338_6180030ae3d6f1dc046d1.png!small?1635779339106

这里判断是否有“zip”或“calg”字段,而我们的是“alg”({"alg":"none"}),快速运行来试一下:

1635779378_618003327344496459f69.png!small?1635779378612

返回"none",而源代码这里,返回的是null。

回到

\io\jsonwebtoken\impl\compression\DefaultCompressionCodecResolver.class#resolveCompressionCodec()

1635779429_61800365e25bb8f63ab2f.png!small?1635779430089

接着往下就返回null了:

1635779454_6180037e40b6e5c843505.png!small?1635779454305

回到

\io\jsonwebtoken\impl\DefaultJwtParser.class#parse()

1635779470_6180038ecf48afc810dfe.png!small?1635779470970

返回的null给到compressionCodec,接着往下:

1635779480_61800398e3370bddea646.png!small?1635779481164

compressionCodec为null,走else分支:

1635779524_618003c4f3dfacaef83ed.png!small?1635779525120

这里就是将刚才存到Payload的第二段Base64编码字符进行Base64解码,保存到payload。

处理后的结果:

1635779559_618003e779f58cff46798.png!small?1635779559664

payload赋值为{"iat":1636552183,"admin":"false","user":"admin"}

接着往下:

1635779632_61800430bda42cbabed2d.png!small?1635779632897

看下这个Claims:

\io\jsonwebtoken\Claims.class

1635779651_6180044303e7a75a48e9f.png!small?1635779651107

对应到Payload标准中注册的声明(建议但是不强制使用):

iss:jwt签发者

sub:jwt所面向的用户

aud:接收jwt的一方

exp:jwt的过期时间,这个过期时间必须要大于签发时间

nbf:定义在什么时间之前,该jwt都是不可用的

iat:jwt的签发时间

jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

接着看这个if:

1635779668_61800454b04b8d2ea61a0.png!small?1635779668790

payload的格式符合要求,可以进入if体:

1635779701_618004759f540accdc2d6.png!small?1635779701700

读取payload,新组一个Map对象:

1635779719_618004877dead3f5e05eb.png!small?1635779719741

接着利用DefaultClaims的构造方法,得到标准Claims:

1635779731_618004933cf51305e8b77.png!small?1635779731387

DefaultClaims实例对象给到claims:

1635779768_618004b89223f8a9d6403.png!small?1635779768797

接着往下:

1635779780_618004c496fb571c49bfe.png!small?1635779780797

由于我们的POC中,删除了第三段:

access_token=eyJhbGciOiJub25lIn0.eyJpYXQiOjE2MzY1NTIxODMsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiYWRtaW4ifQ.

所以,不进入这个if体。

接着往下:

1635779793_618004d1040348421ce48.png!small?1635779793075

这里的this.allowedClockSkewMillis默认为0L,所以allowSkew为false

接着,如果claims不为null,进入if体,校验有效期,这里显然不为null:

1635779814_618004e6071652e12f2fa.png!small?1635779814148

1635779826_618004f26100e9c95df9b.png!small?1635779826723

先获取当前时间,然后调用DefaultClaims的getExpiration方法获取过期异常:

1635779838_618004fe8f831059ce0b2.png!small?1635779838844

传入“exp”调用DefaultClaims的get方法:

1635779850_6180050a2a5614b84b192.png!small?1635779850232

再跟进JwtMap的get方法:

1635779860_61800514bb0fb4e1b801f.png!small?1635779860986

回顾下

exp:jwt的过期时间,这个过期时间必须要大于签发时间

这里找不到“exp”,直接返回null到DefaultJwtParser的parse方法:

1635779871_6180051fedced7aaf8627.png!small?1635779872082

跳过这个if判断,继续往下:

1635779883_6180052b85af32d167bd1.png!small?1635779883712

跟进看看:

1635779897_618005398fcef63cdf53e.png!small?1635779897715

跟上边类似,这次取的是“nbf”

回顾下

nbf:定义在什么时间之前,该jwt都是不可用的

也是返回null:

1635779909_6180054582afc6900f31d.png!small?1635779909581

继续往下:

1635779920_6180055027f0efaa2750d.png!small?1635779920275

从方法名字可看出,校验期望Claims,跟进看下:

1635779933_6180055d4f6c5ede5c124.png!small?1635779933543

默认为空的,所以直接return了:

1635779952_618005701020e4722964f.png!small?1635779952283

再次回到:

1635779966_6180057e4c571e41eb247.png!small?1635779966349

if (base64UrlEncodedDigest != null) {
return new DefaultJws((JwsHeader)header, body, base64UrlEncodedDigest);
} else {
return new DefaultJwt((Header)header, body);
}

关键分支,Digest被我们删掉了

return一个新的DefaultJwt对象:

1635779982_6180058e50fa48c5ca0a7.png!small?1635779982592

DefaultJwt的构造方法:

public DefaultJwt(Header header, B body) {
this.header = header;
this.body = body;
}
再次回到
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);

1635780001_618005a1e65cf167e7606.png!small?1635780002117

看下返回的Jwt实例对象:

1635780012_618005acbcd56f7290781.png!small?1635780012912

接着往下:

1635780028_618005bc7b2f92246eae4.png!small?1635780028985

跟进

\io\jsonwebtoken\impl\DefaultJwt.class#getBody()

1635780044_618005cccbfa486d9749f.png!small?1635780044926

可以看到,直接返回了传入的Payload部分,给到DefaultClaims实例对象claims:

1635780057_618005d9525b15878ddb5.png!small?1635780057509

完事,user被覆盖了:

1635780068_618005e49d14bf3cf9267.png!small?1635780068745

回想下,到现在为止,都没有看到判断“alg”的分支,那我们不修改第一部分的内容试下:

1635780080_618005f0dd19c8e1fbeae.png!small?1635780081093

好吧,只要删除了第三部分就可以成功。

结语

本篇文章只是针对了JWT一个比较老的验签漏洞,做一个分析。要学习JWT框架,涉及的知识还是挺多的,JWT支持各种对称和非对称算法,JWT的JWE和JWS分别对应加密/解密和签名/验签,学习过程还是十分有趣的。

# JWT # Java代码审计 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录