你接手过一个跑了很多年的 Java 项目吗?pom.xml 或 build.gradle 里躺着几十个依赖,有些版本老得连原作者都忘了为什么引入。某天安全公告弹出:你用的 log4j 版本有远程代码执行(RCE),你翻了翻依赖树,发现它是被某个早已不维护的工具包间接带进来的——你根本没直接写过一行 log4j。
这不是个例。Java 生态的漏洞有它的”体质”:组件多、链条长、历史包袱重、利用链成熟。这一篇先把全景摊开,让你知道该盯哪些组件、按什么顺序排查,后面六章再逐个拆开打。
本文所有漏洞分析仅用于你对自有或已授权的系统做安全评估与防御加固。对未授权系统利用下述漏洞属违法。
Java 生态为什么这么容易出事
和 PHP(一个请求一个进程、用完即弃)不同,Java 企业级应用通常是”一个大容器里跑一堆组件”:Web 框架、ORM、JSON 库、日志、中间件、缓存、消息队列……任何一个环节的实现缺陷,都可能成为入口。根因可以归成五类:
- 依赖地狱:一个 Spring Boot 应用实际引入的传递依赖常常上百个。你只写了
spring-boot-starter-web,它背后牵出jackson、tomcat、logback等几十个包。漏洞往往藏在”你没直接引用”的传递依赖里。 - 历史组件债:很多金融、政企系统跑在十年前的
WebLogic、Struts2、Shiro上,升级成本极高,于是老漏洞长期存在。 - 反序列化机制:Java 原生反序列化会自动调用对象生命周期方法,攻击者可构造恶意对象在”读回内存”这一步就触发代码执行,这是 Java 生态独一份的高危面。
- 表达式语言(EL)泛滥:
OGNL、SpEL、MVEL、Freemarker等表达式引擎功能强大,一旦把用户输入拼进表达式并求值,就等价于给用户开了命令执行口子。 - JNDI / RMI 远程加载:Java 的命名目录接口允许从远程 LDAP/RMI 服务器加载类,Log4Shell 正是钻了这个空子。
一张图看懂高危组件分布
文字容易飘,下面这张图把 Java 生态里最常爆雷的组件按”位置”摆了出来,对照着看更踏实:
记住顺序:从入口层(Web 框架/中间件)到数据层(JSON/序列化库),再到基础设施层(日志/JNDI/依赖),每一层都有经典雷区;排查时按”先组件版本、后利用链”的顺序走最稳。
(图中组件:入口层 Struts2/Spring MVC/Shiro;数据层 Fastjson/Jackson/XStream/原生反序列化;基础设施层 Log4j/CommonsCollections/WebLogic/Tomcat。)
常见高危组件速查表
下面这张表是后面六章的”目录”,先混个脸熟:
| 组件 | 代表漏洞 | 类型 | 危害 |
|---|---|---|---|
| Log4j 2.x | CVE-2021-44228 Log4Shell | JNDI 注入 | RCE |
| 原生反序列化 + CommonsCollections | CC 链 | 反序列化 gadget | RCE |
| Fastjson | autoType 绕过 | JSON 类型推断 | RCE |
| Jackson | 默认类型启用 | 多态反序列化 | RCE/信息泄露 |
| Struts2 | S2-045 / S2-061 | OGNL 注入 | RCE |
| Spring | Spring4Shell / SpEL | 数据绑定 / 表达式注入 | RCE |
| Shiro | rememberMe 反序列化 | 硬编码密钥 + 反序列化 | RCE |
| WebLogic | T3/IIOP、CVE-2020-14882 | 反序列化 / 后台 RCE | RCE |
| Tomcat | CVE-2020-1938 Ghostcat | AJP 文件包含 | 文件读取/写入 |
漏洞的生命周期:从发现到被扫
理解漏洞”怎么被用”,能帮你判断自己有多紧急:
- 披露:安全研究者公开 CVE 与原理。
- PoC 流出:有人放出可复现的验证代码。
- 武器化:工具(如
ysoserial、各类利用脚本)把 PoC 包装成一键利用。 - 批量扫描:僵尸网络、挖矿团伙用扫描器全网找存在该组件且未修复的机器。
- 修复滞后:企业升级慢,于是漏洞”长尾”存在数年。
所以漏洞披露后的 72 小时是最危险的窗口——你越早确认”我有没有这个组件、版本是否受影响”,就越主动。
怎么自查:别靠肉眼翻依赖
面对上百个依赖,肉眼不现实。三类手段:
- 看依赖树:
mvn dependency:tree或gradle dependencies,先弄清到底引入了哪些包、什么版本。 - 用 SCA 工具:软件成分分析(Software Composition Analysis)工具能自动比对依赖与 CVE 库。常见的有
OWASP Dependency-Check、Snyk、JFrog X-Ray、GitHub Dependabot。它们会给你一份”哪个包、哪个版本、对应哪个 CVE、严重级别”的报告。 - 订阅告警:关注
CNVD、CNNVD、各组件官方安全公告,把关键组件加进监控。
一个 Dependency-Check 的起步命令:
# 对项目目录生成报告(HTML + JSON)
dependency-check.sh --project my-app --scan ./ --format HTML --format JSON --out ./reports
报告会列出每个带 CVE 的依赖,并给出修复建议版本,是做合规与自检的刚需。
防御总原则:这一篇先记住这几条
- 最小化依赖:没用到的包删掉,传递依赖里不必要的也
exclude掉,攻击面越小越好。 - 锁版本 + 定期升级:用依赖锁文件固定版本,建立季度升级机制,别让组件”自生自灭”。
- 不可信数据不反序列化:来自用户/网络的字节流,优先用 JSON 且关闭类型推断。
- 表达式不拼用户输入:
SpEL/OGNL的parseExpression绝不要把外部字符串直接喂进去。 - 纵深防御:WAF、RASP、最小权限运行,即使某一层失守也不至于直接拿到机器。
排查落地:给你的项目做个组件体检
光知道有哪些雷还不够,关键是你自己的项目里有没有这些雷。下面给一套可立即执行的”体检流程”,照着走一遍:
- 拉依赖清单:
mvn dependency:tree > deps.txt或gradle dependencies > deps.txt,把整棵依赖树落盘。 - 标出高危组件:在清单里搜
log4j-core、fastjson、jackson-databind、struts2、spring-web、shiro、commons-collections、xstream、weblogic、tomcat、commons-beanutils这些名字,记录它们的版本号。 - 对照 CVE:把版本号拿到
CVE Details、CNVD、CNNVD或 SCA 工具里比对,确认是否在受影响区间。 - 查间接引入:很多危险组件是”被别人带进来的”。比如你只引了某个旧工具库,它内部依赖了
commons-collections 3.1,你就要在dependency:tree里顺藤摸瓜找出来,再用<exclusions>排除或升级父依赖。 - 建监控:把上述高危组件加入 Dependabot / Snyk 监控,以后只要新 CVE 一出,工具会主动提醒你。
这套流程不用一次做完美,先跑出第一份清单就是巨大进步——很多团队连”我到底用了哪些组件”都答不上来。
常见误区
- “我没直接用 Fastjson 就安全”:错。它是被别的库间接带进来的概率极高,必须看依赖树确认。
- “升级到修复版本就永远安全”:错。修复版只是修了已知链,新的 bypass 或新 gadget 随时可能出现,监控不能停。
- “内网系统不用管”:错。内网横向移动、供应链投毒、内部人员都能利用,且内网往往更老更脆。
- “WAF 能完全挡住”:WAF 是兜底不是保险箱,编码变形、分块、加密信道都能绕过,根因修复才是关键。
自测题
- 你的项目里,
log4j-core和commons-collections的实际版本是多少?是间接依赖还是直接依赖? - 如果明天爆出一个你正在用的组件的新 CVE,你能在多久内确认”我有没有中招”?
- 你现在的依赖清单(SBOM)在哪里?上次更新是什么时候?
- 说出三个”只靠 WAF 不够、必须升级组件”的漏洞例子。
这一篇你该记住的
- Java 生态漏洞频发,根因是依赖链条长、历史组件多、反序列化与表达式引擎可被利用、JNDI 可远程加载类。
- 高危组件集中在四层:入口框架(Struts2/Spring/Shiro)、数据解析(Fastjson/Jackson/反序列化)、日志基础设施(Log4j)、中间件(WebLogic/Tomcat)。
- 漏洞从披露到被全网扫描很快,披露后 72 小时是黄金自查窗口。
- 自查靠
mvn dependency:tree+ SCA 工具(Dependency-Check/Snyk),不要肉眼翻。 - 防御总纲是:最小化依赖、锁版本、不反序列化不可信数据、表达式不拼用户输入、加纵深防御。
下一篇我们从一个”一行日志就能 RCE”的传奇漏洞讲起——Log4Shell(Log4j JNDI 注入),看看那个让全球连夜加班的 ${jndi:ldap://...} 到底是怎么回事。