JWT身份验证相关安全问题

news2024/11/18 1:47:19

前言:工作中需要基于框架开发一个贴近实际的应用,找到一款比较合适的cms框架,其中正好用到的就是jwt做身份信息验证,也记录一下学习jwt相关的安全问题过程。

 

JWT介绍

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT组成

JWT可分为三部分,分别为头部(header)、载荷(payload)、签名(signature),简单介绍一下每个部分的作用。

整体组成如下,以“.”分隔为三头部、载荷、签名部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT加解密网站:JSON Web Tokens - jwt.io

头部(header)

作用:声明类型、加密算法等

原始格式:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

解base64:

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

typ:声明算法类型,这里是JWT,

alg:声明加密算法,这里是HS256,为对称算法(前后端使用同一密钥进行加密,并非能够解密),常用的还有RS256和ES256两个非对称算法(签名时使用私钥,验证时使用公钥)。

载荷(payload)

作用:存储有效数据

原始格式:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

解base64:

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

载荷部分默认字段:

iss (issuer):JWT的发行者
exp (expiration time):过期时间
sub (subject):JWT面向的主题
aud (audience):JWT的用户
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):JWT唯一标识

注:用户可根据需求自定义字段

签名(signature)

作用:签名部分,服务端校验此字段来验证载荷(payload)字段是否合法

原始格式:SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

该字段加密方式如下:

signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

这里的HMACSHA256就是在header中alg字段指定的HS256加密算法,而RS256和ES256则是服务端使用私钥加密,好处是可以将验证委托给其他应用,只要散发自己的公钥即可。

JWT用法

JWT被用于身份验证中,作用类似于session,但相比于session这种方式各有优劣,下面简述一下JWT的使用流程

一、用户登录,输入登录所需的信息,后端验证后,返回jwt格式的token

二、用户携带token访问需要身份验证的资源或接口

三、服务端验证jwt格式token中的signature(使用相应加密算法重新加密token中的header和payload,验证是否相等)

四、验证成功,允许访问资源

登录获取token代码示例(lin-cms-springboot)

首先入口是login方法

/**
* 用户登陆
*/
@PostMapping("/login")
public Tokens login(@RequestBody @Validated LoginDTO validator, @RequestHeader(value = "Tag", required = false) String tag) {
UserDO user = userService.getUserByUsername(validator.getUsername());
if (user == null) {
throw new NotFoundException(10021);
}
boolean valid = userIdentityService.verifyUsernamePassword(
user.getId(),
user.getUsername(),
validator.getPassword());
if (!valid) {
throw new ParameterException(10031);
}
return jwt.generateTokens(user.getId());
}

判断用户名和密码正确后,将user.getId(即用户的ID)传入generateTokens方法

public Tokens generateTokens(long identity) {
String access = this.generateToken("access", identity, "lin", this.accessExpire);
String refresh = this.generateToken("refresh", identity, "lin", this.refreshExpire);
return new Tokens(access, refresh);
}

根据固定的字段和传入的用户ID,调用generateToken方法获取token(这里调用了两次,分别生成两个token,access_token和refresh_token,后面会讲)

public String generateToken(String tokenType, long identity, String scope, long expire) {
Date expireDate = DateUtil.getDurationDate(expire);
return this.builder.withClaim("type", tokenType).withClaim("identity", identity).withClaim("scope", scope).withExpiresAt(expireDate).sign(this.algorithm);
}

——————以下调用是com.auth0.jwt第三方库中的内容——————

这里反复调用了withClaim方法

public JWTCreator.Builder withClaim(String name, Long value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
...
重载的所有withClaim方法具体内容都一样

因为这个方法返回的还是this,所以可以直接循环调用,是为了生成并绑定不同字段的值。

接着调用了assertNonNull方法、addClaim方法

private void assertNonNull(String name) {
if (name == null) {
throw new IllegalArgumentException("The Custom Claim's name can't be null.");
}
}
​
private void addClaim(String name, Object value) {
if (value == null) {
this.payloadClaims.remove(name);
} else {
this.payloadClaims.put(name, value);
}
}

assertNonNull方法就是判空处理,addClaim方法就将键值对put到payloadClaims这个map对象中,也就是最终生成的payload字段。

这里执行完成后,接着还跳回generateToken方法,因为调用完withClaim方法后,就会调用withExpiresAt(expireDate).sign(this.algorithm),

withExpiresAt方法,就是添加exp字段

public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}

接着进入最关键的sign方法

public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if (algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
if (!this.headerClaims.containsKey("typ")) {
this.headerClaims.put("typ", "JWT");
}
​
String signingKeyId = algorithm.getSigningKeyId();
if (signingKeyId != null) {
this.withKeyId(signingKeyId);
}
​
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}

headerClaims方法就是向header字段中添加typ和alg的值,重点在最终return的地方,接着跟入new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)

JWTCreator类的实例化,其中传入的参数分别是加密算法对象、header、payload

private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {
this.algorithm = algorithm;
​
try {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
mapper.registerModule(module);
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
this.headerJson = mapper.writeValueAsString(headerClaims);
this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
} catch (JsonProcessingException var6) {
throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", var6);
}
}

简单看一下上面的逻辑,就是对算法、header、payload进行绑定,然后接着往下走,跳回上一步的sign方法,在对JWTCreator实例化后,紧接着又调用了sign方法,不过这个sign方法没有传入参数,也就是下面这个方法

private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s.%s", header, payload, signature);
}

方法中对header和payload的生成就是简单的对json数据进行base64编码,最终生成signature字段的操作为this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)),接着跟入sign方法

public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
try {
return this.crypto.createSignatureFor(this.getDescription(), this.secret, headerBytes, payloadBytes);
} catch (InvalidKeyException | NoSuchAlgorithmException var4) {
throw new SignatureGenerationException(this, var4);
}
}

进入到createSignatureFor方法,传入了加密算法、密钥、header和payload

byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretBytes, algorithm));
mac.update(headerBytes);
mac.update((byte)46);
return mac.doFinal(payloadBytes);
}

就不继续往里跟了(里面最后是调javax.crypto.Mac类中的方法进行的加密,调来调去太乱了),大致原理我们已经搞清楚了,差不多就是根据header和payload字段,然后用secret当salt做一次hash,最后再base64编码传出来,就是我们最终的token。

以上就是从登录到获取token的大致过程。

通过jwt进行身份校验代码示例

这里是拦截器中的一个方法,就不一步一步的跟了,

public boolean handleLogin(HttpServletRequest request, HttpServletResponse response, MetaInfo meta) {
//获取请求头中的Authorization头
String tokenStr = verifyHeader(request, response);
Map<String, Claim> claims;
try {
//在这里做的校验
claims = jwt.decodeAccessToken(tokenStr);
} catch (TokenExpiredException e) {
throw new io.github.talelin.autoconfigure.exception.TokenExpiredException(10051);
} catch (AlgorithmMismatchException | SignatureVerificationException | JWTDecodeException | InvalidClaimException e) {
throw new TokenInvalidException(10041);
}
return getClaim(claims);
}

最终验证的地方在com.auth0.jwt.algorithms.HMACAlgorithm#verify方法中

public void verify(DecodedJWT jwt) throws SignatureVerificationException {
byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature());
​
try {
//然后调用verifySignatureFor方法校验
boolean valid = this.crypto.verifySignatureFor(this.getDescription(), this.secret, jwt.getHeader(), jwt.getPayload(), signatureBytes);
if (!valid) {
throw new SignatureVerificationException(this);
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException var4) {
throw new SignatureVerificationException(this, var4);
}
}

跟入

boolean verifySignatureFor(String algorithm, byte[] secretBytes, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
return this.verifySignatureFor(algorithm, secretBytes, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes);
}
​
boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
return MessageDigest.isEqual(this.createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), signatureBytes);
}

跟入

byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretBytes, algorithm));
mac.update(headerBytes);
mac.update((byte)46);
return mac.doFinal(payloadBytes);
}

可以看到,这里的createSignatureFor方法,那么最终实现的原理也就是根据传入的header和payload,再调用创建签名方法根据secret密钥创建签名,如果最终创建出来的签名和你传入的jwttoken中的Signature字段值相等,则判定为真。

JWT安全问题

secret硬编码

将加密使用的secret密钥硬编码在框架中,如果开发者不注意的话,使用默认密钥,没有进行修改,那么只要获取到密钥,就可以伪造token。

知道密钥后,那么可以通过该网站https://jwt.io或者编写脚本伪造token

例如该cms:

1659882676_62efccb4e7be5d7abeaa3.png!small?1659882677806

通过在线网站生成token:

1659882686_62efccbe2184e186d88e3.png!small?1659882686985

那么我们直接使用这个token即可访问网站中需要身份验证的接口资源。

1659882697_62efccc9a29fc0fb41501.png!small?1659882698632

后端未校验Signature字段或

攻击方法:可任意修改或者直接删除

原理:后端未对Signature字段进行校验,就取payload中的数据进行后续操作

alg=none签名绕过漏洞(CVE-2015-2951)

攻击方法:将header中的alg的键值改为none,然后将Signature删除即可

原理:后端未对传入的header中的alg字段进行校验,直接使用其中指定的加密算法对Signature

针对以上两种安全问题,如果没有原始jwt_token,可以使用如下脚本生成token:

使用之前需要先使用pip安装PyJWT,而不是JWT,直接python37 -m pip install PyJWT即可

import jwt
payload = {
"identity": 1,
"scope": "lin",
"type": "access",
"exp": 1659797574
}
print(jwt.encode(payload,None,algorithm="none"))

Secret爆破

上面我们知道硬编码的话我们可以任意伪造token,其实原理就是知道secret密钥,除了开源CMS的这种泄露,或者其他系统备份文件、日志之类的泄露密钥,我们还可以通过爆破的方法获取密钥(如果不是弱密钥,难度非常大)

可以使用这个工具:https://github.com/ticarpi/jwt_tool

修改非对称密码算法为对称密码算法(CVE-2016-10555)

这个漏洞只针对使用非对称加密算法(RS256)做校验的系统,当后端使用RS256加密时,使用私钥加密,而校验时用到的是公私钥对中的公钥做校验,而公钥是公开的,我们很容易获取。

那么当我们修改RS256为HS256时,后端会以为使用的是HS256对称加密做校验,即使用公钥当作HS256校验时的secret来进行加密对比是否相等,把公钥当成secret来使用,也就相当于泄露了secret,所以我们就可以使用公钥来伪造token。

伪造密钥(CVE-2018-0114)

我理解这个漏洞和上面的漏洞比较像,上一个漏洞是后台新人了我们提供的header中的算法,这个是使用了JWS,里面也是存储的公钥,那么我们自己生成公私钥对,然后使用私钥生成token,再将自己生成的公钥放到JWS中,让后台使用这个公钥解密,这样就可以巧妙地绕过后台的验证。

网络安全学习资源分享:

给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

因篇幅有限,仅展示部分资料,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,需要点击下方链接即可前往获取

CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

同时每个成长路线对应的板块都有配套的视频提供: 

大厂面试题

视频配套资料&国内外网安书籍、文档

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料

所有资料共282G,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,可以扫描下方二维码或链接免费领取~ 

 读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

特别声明:

此教程为纯技术分享!本教程的目的决不是为那些怀有不良动机的人提供及技术支持!也不承担因为技术被滥用所产生的连带责任!本教程的目的在于最大限度地唤醒大家对网络安全的重视,并采取相应的安全措施,从而减少由网络安全而带来的经济损失。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1713420.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何遍历并处理不平衡的Python数据集

目录 一、引言 二、不平衡数据集的概念与影响 三、处理不平衡数据集的策略 重采样策略 集成学习方法 代价敏感学习 一分类方法 四、Python工具与库 五、案例分析与代码实现 案例一&#xff1a;使用imbalanced-learn库进行上采样 案例二&#xff1a;使用scikit-learn…

知识付费小程序源码系统 界面支持万能DIY装修,一站式运营 附带完整的源代码以及搭建教程

系统概述 这是一款功能强大的知识付费小程序源码系统&#xff0c;它为用户提供了一个全面的平台&#xff0c;能够满足各种知识付费场景的需求。其界面支持万能 DIY 装修&#xff0c;让用户可以根据自己的品牌形象和风格进行个性化定制&#xff0c;打造出独具特色的小程序界面。…

台灯护眼是真的吗?台灯怎么选对眼睛好?看这一篇就够了

随着现代生活方式的改变&#xff0c;孩子们面临着越来越多的视力挑战。我们一直使用普通台灯&#xff0c;往往忽略了不合适的台灯也会给孩子眼部健康带来危害。普通台灯&#xff0c;尤其是使用白炽灯或荧光灯作为光源的台灯&#xff0c;会发射出紫外线。这些紫外线辐射对孩子的…

HTML新春烟花盛宴

目录 写在前面 烟花盛宴 完整代码 修改文字

内存卡频频提示格式化?数据恢复全攻略

内存卡提示需要格式化 在数字时代&#xff0c;内存卡作为我们存储数据的常用设备&#xff0c;广泛应用于手机、相机、无人机等多种设备中。然而&#xff0c;不少用户在使用过程中会突然遭遇一个令人头疼的问题——内存卡提示需要格式化。这一提示往往伴随着数据的丢失风险&…

端午节趣味互动小游戏的作用是什么

端午节吃粽子&#xff0c;多数行业商家都可借势进行品牌营销&#xff0c;而一场营销效果的优劣&#xff0c;除了好方案外&#xff0c;还需要好的工具/渠道及运营等&#xff0c;围绕粽子元素的互动小游戏是营销互动的主要形式之一。 运用【雨科】平台拥有多款端午节粽子主题互动…

STM32-10-定时器

STM32-01-认识单片机 STM32-02-基础知识 STM32-03-HAL库 STM32-04-时钟树 STM32-05-SYSTEM文件夹 STM32-06-GPIO STM32-07-外部中断 STM32-08-串口 STM32-09-IWDG和WWDG 文章目录 一、STM32 基础定时器1. 基本定时器简介2. 基本定时器框图3. 基本定时器相关寄存器4. 定时器溢出…

单链表,双向链表,循环链表

文章目录 链表单链表双向链表循环链表链表的底层结构链表的实现代码 链表 链表分为单链表&#xff0c;双向链表&#xff0c;循环链表。 单链表 如上图所示&#xff0c;链表是由多个节点组成&#xff0c;节点由数据域与指针域组成&#xff0c;数据域用于存储数据&#xff0c;指…

服务器主机托管一站式托管服务有哪些?

服务器主机托管一站式托管服务&#xff0c;作为现代企业信息化建设的重要一环&#xff0c;为企业提供了一种高效、安全、可靠的服务器运行环境。下面&#xff0c;我们将从多个方面详细介绍这一服务的内容。 一、硬件与基础设施 服务器主机托管服务首先涵盖了服务器硬件和网络基…

windows环境redis未授权利用手法总结

Redis未授权产生原因 1.redis绑定在0.0.0.0:6379默认端口&#xff0c;直接暴露在公网&#xff0c;无防火墙进行来源信任防护。 2.没有设置密码认证&#xff0c;可以免密远程登录redis服务 漏洞危害 1.信息泄露&#xff0c;攻击者可以恶意执行flushall清空数据 2.可以通过ev…

前端Vue自定义个性化导航栏菜单组件的设计与实现

摘要&#xff1a; 随着前端技术的飞速发展和业务场景的日益复杂&#xff0c;组件化开发已成为提升开发效率和降低维护成本的关键手段。本文将以Vue uni-app平台为例&#xff0c;介绍如何通过自定义导航栏菜单组件&#xff0c;实现业务逻辑与界面展示的解耦&#xff0c;以及如何…

25 使用MapReduce编程了解垃圾分类情况

测试数据中1表示可回收垃圾&#xff0c;2表示有害垃圾&#xff0c;4表示湿垃圾&#xff0c;8表示干垃圾。 统计数据中各类型垃圾的数量&#xff0c;分别存储可回收垃圾、有害垃圾、湿垃圾和干垃圾的统计结果。 &#xff08;存储到4个不同文件中&#xff0c;垃圾信息&#xff0…

高效记录收支明细,预设类别账户,智能统计财务脉络,轻松掌握个人财务!

收支明细管理是每位个人或企业都必须面对的财务任务&#xff0c;财务管理已经成为我们生活中不可或缺的一部分。如何高效记录收支明细&#xff0c;预设类别账户&#xff0c;智能统计财务脉络&#xff0c;轻松掌握个人财务&#xff1f;晨曦记账本为您提供了完美的解决方案&#…

JVM的垃圾回收机制--GC

垃圾回收机制&#xff0c;是java提供的对于内存自动回收的机制。java不需要像C/C那样手动free()释放内存空间&#xff0c;而是在JVM中封装好了。垃圾回收机制&#xff0c;不是java独创的&#xff0c;现在应该是主流编程语言的标配。GC需要消耗额外的系统资源&#xff0c;而且存…

“2024深圳数字能源展”共同探讨数字能源未来发展方向和挑战

在数字化浪潮的推动下&#xff0c;新一轮科技革命与产业革命正以前所未有的速度加速兴起。在这个时代背景下&#xff0c;数字化技术与能源行业的高度融合&#xff0c;使得能源数字化成为未来发展的必然趋势。数字经济浪潮的蓬勃兴起&#xff0c;为能源行业的数字化转型提供了强…

网络安全-钓鱼篇-利用cs进行钓鱼

一、环境 自行搭建&#xff0c;kill&#xff0c;Windows10&#xff0c;cs 二、原理 如图所示 三、钓鱼演示 首先第一步&#xff1a;打开System Profiler-分析器功能 选择克隆www.baidu.com页面做钓鱼 之后我们通过包装域名&#xff0c;各种手段让攻击对象访问&#xff1a;h…

XXE漏洞详解——进阶篇

读取文件时有特殊符号 在读取文件时&#xff0c;文件中包含"<,>,&"等这些特殊符号时&#xff0c;会被xml解析器解析&#xff0c;报错从而导致读取失败&#xff0c;例如尝试读取以下文件 C:\test.txt 内容&#xff1a; <Baize Sec> payload: <…

搜维尔科技:Movella Xsens用于动画,CG,短视频制作案例

用户名称 广州百漫文化传播有限公司 应用场景 基于Xsens MVN Link 动作捕捉系统的动画制作、CG制作、短视频制作、快速动画MAYA插件、影视动漫实时合成预渲染。 现场照片 《西行纪》内容简介&#xff1a;在远古神明的年代&#xff0c;世间存在着天众、龙众、阿修罗等八部众…

探索 ChatboxAI:智能对话的新时代

在人工智能迅速发展的今天&#xff0c;智能对话已经成为了我们日常生活中不可或缺的一部分。从智能助理到聊天机器人&#xff0c;AI 技术正在改变我们与世界互动的方式。今天&#xff0c;我们要介绍的是一个全新且功能强大的平台——ChatboxAI。 什么是 ChatboxAI&#xff1f;…

S32K --- FLS MCAL配置

一、前言 二、MCAL配置 添加一个Mem_43_infls的模块, infls是访问内部flash, exfls是访问外部flash 2.1 General 这边暂时保持的默认,还没详细的去研究,等有空研究了,我再来更新 2.2 MemInstance 双金“index”的下标“0”可以进里面详细配置,这个是基本操作了。 2.2.1 G…