2022 年 3 月 29 日,有人在 GitHub 上放出了 Spring Framework 的利用代码,随后删除。截图已经传开。第二天开始,全网扫描器都在找 Tomcat。
Spring 在 3 月 31 日发布 5.3.18 与 5.2.20,CVE-2022-22965 由此定号。前后不到 48 小时,这是近几年节奏最快的一次框架级应急。
背景:披露时间线#
| 时间 | 事件 |
|---|---|
| 3 月 29 日 | PoC 在 GitHub 出现后被删除,截图流出 |
| 3 月 30 日 | 社区复现,扫描量明显上升 |
| 3 月 31 日 | Spring 发布 5.3.18 / 5.2.20,CVE-2022-22965 公开 |
| 4 月初 | Tomcat、各发行版跟进,WAF 规则陆续上线 |
时间线值得复盘的地方在于:官方公告出来之前,防守方手里只有传言和截图。真正有用的动作是提前盘点“哪些应用是 WAR + 外置 Tomcat + JDK 9 以上”,而不是等 WAF 规则。
这条经验后来被反复验证:漏洞披露的第一个小时,资产清单比扫描器值钱。
名字是社区起的。因为和 Log4Shell 撞在同一时期,又都发生在 Java 生态,就被叫成了 Spring4Shell。名字传播得快,也带来混淆——很多人以为它是 Spring 表达式注入,其实是参数绑定,和 SpEL 没有关系。
漏洞原理:数据绑定能碰到类加载器#
点到为止。Spring MVC 的参数绑定会把请求参数按名字写进对象属性,支持嵌套路径。问题在于绑定器没有限制可以访问的属性链。
攻击者可以沿着 class → module → classLoader 一路走到 Tomcat 的内部对象,再通过日志组件的配置字段,把内容写进 webapps 目录下的 JSP 文件。写进去的 JSP 会被容器当普通页面执行。
三个前提缺一不可:
- JDK 9 及以上,模块化之后属性链才可达;
- 以 WAR 包部署在 Tomcat 上;
- 控制器用对象接收参数,这是最常见的写法。
内嵌 Tomcat 的 Spring Boot 默认打成可执行 jar,没有 webapps 目录,已知的利用路径不成立。但这不代表可以不打补丁——属性链还能通向别的地方。
为什么 JDK 8 不受影响?JDK 9 引入模块化之后,Class 才有了 getModule(),属性链才连得下去。这也是很多老系统在漏洞公开当天“自查无风险”的原因,但升级 JDK 的计划要把这条考虑进去。
影响范围#
| 形态 | 判断 | 说明 |
|---|---|---|
| WAR + 外置 Tomcat + JDK 9+ | 高危 | 已知路径成立 |
| Spring Boot 可执行 jar | 较低 | 已知路径不成立,仍需升级 |
| 纯 JSON API,不用表单绑定 | 较低 | 但别赌其他属性链不存在 |
| Spring Cloud Function | 另案处理 | 注意 CVE-2022-22963,别混为一谈 |
具体受影响版本与修复版本请以 VMware/Spring 官方公告为准。本节只给判断标准,不给版本清单,因为版本清单过时得比文章快。
内网系统不能因为“外网访问不到”就延后。扫描器会从办公网、VPN、甚至供应链跳板进来,历史上有不少案例是内部测试环境先被打穿。补丁排期按暴露面分级,但别把内网当免死金牌。
检测#
# 1. 版本(含传递依赖)
mvn -q dependency:tree -Dincludes=org.springframework:spring-webmvc
# 2. 部署形态:是 WAR 还是 fat jar
ls -l /opt/tomcat/webapps/ 2>/dev/null
# 3. 访问日志里的属性链特征
grep -aE 'class\.(module|classLoader)' \
/var/log/tomcat*/localhost_access_log.* 2>/dev/null | tail
# 4. 最近被创建的 JSP —— 利用会落地文件
find /opt/tomcat/webapps -name '*.jsp' -mtime -7 -ls
第四条最直接。如果 webapps 里出现了你没部署过的 JSP,基本可以按失陷处理,不用再纠结日志。
第三条受限于日志格式:默认的 access log 只记 URL,不记参数体。如果用了 POST 或者参数在 body 里,grep 抓不到,需要在 WAF 侧看。
另外要区分两个容易混淆的编号。CVE-2022-22965 是这次的数据绑定问题,CVE-2022-22963 是 Spring Cloud Function 的 SpEL 注入。当时的扫描器两个都打,别看到告警就以为中的是同一个。
如果日志已经滚动覆盖了那几天,退一步查文件系统:webapps 下所有 JSP 的修改时间、Tomcat 的 work 目录、以及有没有人往 logs 目录里塞过异常文件。攻击成功后写进去的 JSP 名字往往很随意,一眼就能看出来。
WAF 侧也要看。把 class.module.classLoader、class.classLoader 这类特征加进规则,即使应用没升级,也能挡住大部分自动化扫描。规则要处理大小写混淆和 URL 编码,别只写一条字面量。
告警要按路径分诊。命中静态资源或首页的请求大多是误报,命中表单提交接口的才需要跟进。把告警和资产清单对一遍,比逐条研判快得多。
修复与缓解#
- 升级到 5.3.18 及以上、5.2.20 及以上。这是唯一根治方案。
- 不能立即升级时,加全局绑定限制。
// 全局禁止绑定 class.* 属性链
@ControllerAdvice
public class BinderAdvice {
@InitBinder
public void lockDown(WebDataBinder binder) {
binder.setDisallowedFields(
"class.*", "Class.*", "*.class.*", "*.Class.*");
}
}
这个写法要求 @ControllerAdvice 被组件扫描到,并且对 @ModelAttribute 生效的路径都覆盖。上线前用一个测试接口验证:提交带 class.module.classLoader 的参数,应该被忽略或返回 400。
setDisallowedFields 只堵住已知的属性链,属于过渡手段。属性链研究不会停,排期升级才是正事。
其他缓解手段:把 Tomcat 升级到已阻断该属性访问的版本;降级到 JDK 8(现实中很少可行);在 WAF 拦 class.module.classLoader 特征(只能挡脚本小子,改个大小写就绕了)。
升级时注意版本对齐。Spring Boot 的依赖管理会把 spring-webmvc 拉到对应版本,只改 Boot 版本而不清理依赖,可能出现两个版本的 jar 同时在场。用 dependency:tree 确认唯一性,再发布。
验证#
- 版本核对:
dependency:tree输出、实际运行的 jar 名、容器镜像 tag 三者一致。很多事故是“改了 pom 没重新构建”。 - 功能回归:表单绑定、文件上传、自定义
Editor、@InitBinder原有逻辑是否受影响。 - 安全验证:向测试接口提交属性链参数,确认被拒绝;再确认 JSP 目录没有新增文件。
- 复查 4 月以来的访问日志,看有没有
class.module特征,以及异常 500。 - 留一条回滚路径:升级前把旧包与配置归档,灰度节点出问题时能在五分钟内切回。安全修复的回滚窗口通常很短,因为漏洞已经公开。
结论#
- Spring4Shell 的窗口期极短,靠的是提前盘点资产,不是临时扫描。
- 三个前提缺一不可,判断优先级先看部署形态,再看 JDK 版本。
- 根治是升级;
setDisallowedFields是过渡,不是终点。 - 检测最有效的信号是
webapps里多出来的 JSP。 - 别把它和 Spring Cloud Function 的 SpEL 漏洞混成一个编号。
完