Administrator
发布于 2022-11-20 / 3927 阅读
23

Spring4Shell 漏洞应急响应与修复

周五下午的告警

2022-03-31 Spring4Shell(CVE-2022-22965)被公开,我们安全群里炸了。我负责的两个服务跑在 Spring Boot 2.3 上,得立刻确认是否在影响范围内。应急响应的第一步不是升级,而是确认影响面——盲目全量升级可能引入别的问题,先搞清楚"我中没中"更关键。

漏洞原理

CVE-2022-22965 是 Spring Framework 的数据绑定(DataBinder)缺陷。当应用部署在 Tomcat 且用 WAR 包、使用 POJO 参数绑定时,攻击者能借助 class.module.classLoader 这种嵌套属性路径,篡改 Tomcat 的 AccessLogValve,把 webshell 写进 web 目录。本质是"能绑定到不该绑定的属性"。

具体链路:Spring 的绑定支持递归路径访问 JavaBean 属性,而 Class 类的 classLoader 通过 module 可达。攻击者构造一个表单字段名 class.module.classLoader.resources.context.parent.pipeline.first.suffix 之类,就能改 Tomcat 的访问日志配置,让它将请求内容按 .jsp 写进 webapp 目录,从而植入 webshell。这个利用链相当精巧,但前提是 Web 容器是 Tomcat 且以 WAR 部署。

影响版本

  • Spring Framework 5.3.x < 5.3.18
  • Spring Framework 5.2.x < 5.2.20
  • 且以 WAR 部署在 Tomcat 上、使用 SpringMVC/WebFlux 参数绑定

我们确认:用 JAR 包内嵌 Tomcat 的 Spring Boot 服务,默认不受影响(classLoader 绑定被 disallowedFields 挡掉了)。但为保险还是全量升级。这里有个认知误区:很多人以为"用了 Spring 就中招",其实部署形态比版本号更关键。

临时防护

来不及升级的服务,先在 WebMvcConfigurer 里加黑名单兜底:

@Override
public void addBinders(WebDataBinderRegistry registry) {
    registry.setDefaultDisallowedFields(
        "class.*", "*.class.*", "*.Class.*");
}

这能拦住大多数利用,但属于临时止血,不能替代升级。

升级与验证

把 Spring Boot 升到 2.3.12.RELEASE(带 5.3.18),灰度一个节点后用脚本打 POC:

curl -X POST http://svc/module \
  -d 'class.module.classLoader...'  # 返回 400,防御生效

从确认到全量升级用了 6 小时,两个核心服务各重启一次,无业务中断。验证环节不能省——我们见过有人升级完以为没事,结果依赖锁版本没生效,其实还跑着旧 Framework。

复盘:我们漏了什么

这次暴露出一个流程问题:我们的依赖漏洞靠人工盯安全公告,从公开到确认影响面花了 2 小时。后来我们把"依赖漏洞自动巡检"补进了 CI,用 OWASP Dependency-Check 每天扫一遍,命中 CVE 直接卡构建。这样下次再有类似漏洞,工具会先告诉我们"哪些服务、哪个版本、是否可达"。

另外,部署形态统一也很关键。我们那两个中招风险的服务恰好是老 WAR 包,借着这次机会把它们也改成了内嵌 Tomcat 的 JAR 部署,从根上消除了这一类攻击面。

影响面判定的细节

判定"中没中"时还有个易错点:光看 Spring 版本不够,得看是不是"用数据绑定接收用户输入的 POJO"。我们有些内部接口用的是 @RequestBody(JSON 反序列化,走 Jackson,不受影响),只有用表单绑定(@ModelAttribute)且字段直接映射到 POJO 的才危险。所以即便版本在范围内,没用表单绑定的也基本安全。我们据此把风险服务缩小到 3 个老接口,优先处理,其余观察即可,避免全员慌乱升级。

临时防护的验证

加 disallowedFields 黑名单后,一定要用真实 POC 验证真的拦住了,而不是"配了就以为行"。我们验证时发现一个变体 class.module.classloader(小写 L),黑名单里写的是 class.* 能覆盖,但有人写成更窄的规则就漏了。验证环节用自动化脚本打多种变形,确认全部 400 才放心。安全加固最怕"看起来配了"。

长期机制

这次之后我们建了两道防线:一是 CI 里跑 OWASP Dependency-Check,每天扫依赖,新 CVE 命中直接卡构建并飞书告警;二是所有对外服务统一改成 JAR 内嵌 Tomcat 部署,从部署形态上消除"WAR + Tomcat"这类攻击面。前者让我们第一时间知道"谁有漏洞",后者让我们即使有漏洞也少一个可利用路径。应急响应从"事后救火"变成"事前布防"。

时间线的复盘

把这次响应拉成时间线,给后来者参考:T0 漏洞公开,T+30min 确认影响面(3 个老接口中招),T+2h 临时防护上线,T+4h 灰度升级完成,T+6h 全量升级 + POC 验证闭环。总共 6 小时,无业务中断。关键是前 30 分钟没盲目升级,先定性,避免了"升级引入新 bug"的二次事故。应急响应的节奏比速度重要。

给同行的建议

如果你的服务是 Spring Boot 内嵌 Tomcat 的 JAR 部署,基本不用担心 Spring4Shell,但也建议升到安全版本,别赌"部署形态护体"。如果是 WAR 部署在独立 Tomcat,立刻评估、立刻升级。另外订阅 Spring 的安全公告邮件列表,漏洞公开当天就能收到,比从群里看到快几小时。安全响应慢那几小时,可能就是被扫进的范围。我们后来把安全公告订阅和 CI 巡检都自动化了,基本不用人工盯。

小结

应急响应最忌慌乱。先确认影响面(部署形态比版本号更关键),临时防护与升级双线并行,再用 POC 验证闭环。这次让我们把"依赖漏洞自动巡检"补进了 CI,也顺手统一了部署形态。安全不是一次升级,而是把"发现-确认-修复-验证"固化成机制。

参考