拿到一套 Java 源码,不要先翻 Controller。
一套中型系统的 Controller 加 Service 有几百个类,读懂业务逻辑才能判断有没有洞。但依赖版本和配置项不用懂业务——版本号就写在那里,配置项就摆在那里,对照漏洞库和默认值清单就能得出结论。
所以前 30 分钟我只看三个文件:pom.xml、application.yml、web.xml。
这 30 分钟不是为了直接找到漏洞,是为了划重点——产出是一张"哪些地方值得深挖"的清单。
0x01 依赖
版本号不在子模块里
多模块项目的第一坑:你在 ruoyi-admin/pom.xml 里翻半天找不到版本号。
<!-- 根 pom:pom.xml -->
<properties>
<shiro.version>1.13.0</shiro.version>
<fastjson.version>2.0.57</fastjson.version>
<poi.version>4.1.2</poi.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 子模块:ruoyi-framework/pom.xml —— 只有 artifactId,没有 version -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
版本号集中在根 pom 的 <properties> 里,子模块全部继承。直接看根 pom 的 properties 段,比逐个模块翻快十倍。
pom 会骗你:三种情况
第一,间接依赖不写在 pom 里。
SnakeYAML 是典型。SpringBoot 解析 YAML 配置需要它,所以它是自动带进来的,pom.xml 里一个字都没有。只扫 pom 的话,你根本不知道项目里有这东西。
mvn dependency:tree -DoutputFile=deps.txt
grep -iE "snakeyaml|jackson|commons-(collections|beanutils)" deps.txt
第二,版本号写着老版本,实际 jar 被换过。
二开项目里很常见:pom 里是 1.2.24,但 WEB-INF/lib/ 下的 jar 是 1.2.83;或者依赖被 <exclusions> 排除了,运行时用的是容器提供的版本。
以运行时实际加载的 jar 为准。 只有源码没有构建环境时,直接看 WEB-INF/lib/ 下的文件名,jar 包名里一般带版本号。
第三,本地私有依赖。
<dependency>
<groupId>com.company</groupId>
<artifactId>company-sso-sdk</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/company-sso-sdk-1.0.jar</systemPath>
</dependency>
scope=system 的依赖不会进依赖树,SCA 工具扫不到。这类私有 SDK 反而经常是自己写的加密、认证逻辑,值得单独看一眼。
版本对照
优先确认这几个组件。版本号落在下面的范围里就要警觉。范围以官方安全公告为准。
| 组件 | 受影响范围 | 风险 |
|---|---|---|
| Shiro | ≤ 1.2.4 | 硬编码 AES 密钥,rememberMe 反序列化(Shiro-550) |
| Shiro | < 1.4.2 | Padding Oracle(Shiro-721) |
| Shiro | ≤ 1.8.0 | 多个路径匹配导致的权限绕过 |
| Fastjson | ≤ 1.2.80 | AutoType 多轮绕过,最终演变成通杀 |
| Jackson-databind | 2.9.x 及以下高危集中 | 开启 defaultTyping 后反序列化 RCE |
| Log4j2 | 2.0-beta9 ~ 2.14.1 | Log4Shell(CVE-2021-44228) |
| Struts2 | 2.3 ~ 2.3.34、2.5 ~ 2.5.16 | OGNL 表达式注入 RCE(S2-045 / S2-057 等) |
| Commons-Collections | 3.0 ~ 3.2.1 | 反序列化利用链的弹药库 |
| Commons-Beanutils | < 1.9.4 | 反序列化利用链 |
| XStream | < 1.4.18 | 反序列化 RCE |
| SnakeYAML | < 2.0 | Yaml.load() 反序列化(CVE-2022-1471) |
| POI | < 3.11 | XXE / DoS |
| Spring Framework | < 5.3.18、< 5.2.20 | Spring4Shell(CVE-2022-22965) |
| Spring Cloud Gateway | < 3.1.0、< 3.0.6 | Actuator SpEL RCE(CVE-2022-22947) |
SCA 报的 23 个漏洞,我为什么只留了 3 个
有次审计一套若依二开系统,SCA 工具报了 23 个组件漏洞。逐条核实后我只留了 3 个。
排除分三类。
有漏洞组件,但没有触发入口。
报了 POI 的 XXE。翻完所有 Controller,项目只做了 Excel 导出,没有导入。导出不解析用户可控的 XML,不可利用。
有漏洞组件,但代码路径不可达。
报了 Jackson 反序列化。项目里所有 JSON 解析都走 SpringMVC 默认的 MappingJackson2HttpMessageConverter,全局搜不到 enableDefaultTyping。Jackson 的绝大多数 CVE 都依赖 defaultTyping 被显式开启,不开就是安全的。
版本号看起来受影响,实际被处理过。
pom 里写着老版本,但 lib/ 下的 jar 被替换过。
这次审计真正被打穿的,是一个 SCA 完全不会报的位置:若依定时任务的 invokeTarget 支持类全路径实例化:
// ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java
public static void invokeMethod(SysJob sysJob) throws Exception {
String invokeTarget = sysJob.getInvokeTarget();
String beanName = getBeanName(invokeTarget);
String methodName = getMethodName(invokeTarget);
List<Object[]> methodParams = getMethodParams(invokeTarget);
if (!isValidClassName(beanName)) {
Object bean = SpringUtils.getBean(beanName);
invokeMethod(bean, methodName, methodParams);
} else {
Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
invokeMethod(bean, methodName, methodParams);
}
}
Class.forName(beanName).getDeclaredConstructor().newInstance() —— 类名可控就等于任意类实例化。SnakeYAML 本身不是"有漏洞的组件",它只是被用错了地方,所以任何 SCA 工具都不会提示这里。
SCA 的作用是告诉你哪些组件要警觉,不是告诉你哪里有漏洞。这两件事必须分开。
0x02 配置
配置是投入产出比最高的部分,因为大部分配置问题不需要理解业务就能定性。
若依:token.secret 用的还是文档默认值
ruoyi-admin/src/main/resources/application.yml:
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期(默认30分钟)
expireTime: 30
这是若依文档里的默认值。RuoYi-Vue 用 JWT 做认证,签名密钥就是 token.secret。没改的话,攻击者可以自己签发任意用户的 JWT,包括 admin,不需要知道任何密码。
审计动作很简单:把这个值和官方文档里的默认值比对。一样就是问题。
application-druid.yml 里的 root
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
username: root,password: password,都是文档默认值。实际部署里照抄的比例不低。
要记录的是三件事:账号是不是 root、密码是不是默认值、3306 是否对外。
Druid 监控页面
同一个文件往下:
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
enabled: true,allow 留空等于不限制来源 IP,口令是 ruoyi/123456。
但光看这段还不能定性——关键在授权配置里 /druid/** 有没有被放行。若依的 SecurityConfig 里历来有这么一段:
.requestMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**",
"/*/api-docs", "/druid/**").permitAll()
permitAll 加上默认口令,就是未授权访问 Druid 控制台。SQL 监控里带着完整的 SQL 语句和参数,Session 监控里带着在线用户。
具体到你手上的版本,去 SecurityConfig(RuoYi-Vue)或 ShiroConfig(RuoYi 单体版)里确认这一段。
同一个文件里的 Swagger
# Swagger配置
swagger:
# 是否开启swagger
enabled: true
# 请求前缀
pathMapping: /dev-api
enabled: true。生产环境开 Swagger,等于把全部接口、参数名、参数类型整理好交给对方。配合上面那段 permitAll,/v2/api-docs 直接可以匿名拉取。
MCMS:Shiro 密钥写在配置文件里
MCMS 5.2.8 是 Spring + MyBatis + Shiro + FreeMarker 的组合,Shiro 版本 1.8.0,AES 密钥写在 application.yml 里,不是硬编码在代码中。
这两种情况要区别对待:
硬编码在 AbstractRememberMeManager 里的默认密钥(kPH+bIxk5D2deZiIxcaaaA==),攻击者不需要看你的源码就知道。
写在配置文件里的密钥,取决于部署时改没改,以及配置文件有没有泄露途径。
所以判断顺序是:密钥是否可预测 → Shiro 版本 → 依赖里有没有可用的 gadget 链。 三个条件缺一不可。
只看到 Shiro 1.8.0 就判高危,是常见的误报来源。反过来,只看到密钥是默认值但没有可用链,也打不穿。
JDBC URL 的两个参数
url: jdbc:mysql://x.x.x.x:3306/db?autoDeserialize=true&allowLoadLocalInfile=true&allowUrlInLocalInfile=true
这不是版本漏洞,是配置漏洞。
autoDeserialize=true 配合恶意的 MySQL 服务端可以触发反序列化 RCE;allowLoadLocalInfile=true 是任意文件读取。利用前提是 JDBC 连接的地址可控(比如后台有数据源配置功能,或者连接了不可信的内网 MySQL)。
审计时直接搜这两个参数名。SCA 永远不会报,但实战里确实能用。
0x03 web.xml
SpringBoot 项目可能没有 web.xml,这一步跳过。如果是 war 部署的老项目(比如 MCMS 的 war 模式),这一步不能省。
原因:SpringBoot 的 Controller 是扫描注册出来的,而 web.xml 里的 Servlet 是手工声明的。
<servlet>
<servlet-name>fileExportServlet</servlet-name>
<servlet-class>net.mingsoft.cms.servlet.FileExportServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>fileExportServlet</servlet-name>
<url-pattern>/export/*</url-pattern>
</servlet-mapping>
手工声明的 Servlet 通常不走框架的过滤器和拦截器链。它可能不受 Shiro / Spring Security 保护,可能是某个开发为了临时需求加的,从没经过评审,而且它的 URL 在 Java 代码里搜不到(映射写在 XML 里),工具很容易漏。
grep -n "servlet-class\|filter-class\|url-pattern" web.xml
逐个回答一个问题:这个 URL 需要登录吗。
0x04 这 30 分钟的清单
依赖:
[ ] 根 pom 的 properties 段,版本号全部摘出
[ ] mvn dependency:tree,检查间接依赖(重点 snakeyaml、jackson、commons-*)
[ ] scope=system 的私有依赖单独列出
[ ] WEB-INF/lib/ 下的实际 jar 版本,与 pom 比对
[ ] SCA 结果逐条核实可达性,分"可利用 / 需确认 / 不可达"三档
[ ] 确认是否存在代码生成、定时任务、动态数据源这类"能间接执行代码"的模块
配置:
[ ] 数据库账密,是否 root,3306 是否对外
[ ] 全部 secret / key / password,与文档默认值比对
[ ] Actuator 暴露范围:management.endpoints.web.exposure.include
[ ] Swagger 是否开启
[ ] Druid / 其他监控页面是否开启,白名单是否为空
[ ] JDBC URL 中的 autoDeserialize、allowLoadLocalInfile
[ ] 上传路径与后缀白名单
[ ] 上述"是否开启"的项,去 SecurityConfig / ShiroConfig 里确认是否放行
web.xml:
[ ] 手工声明的 Servlet / Filter 全部列出
[ ] 每个 URL 的鉴权状态
产出:一张"重点跟进清单",后面进代码阶段按这张单子走
下一篇进代码,讲怎么把几百个类收敛成一张能用的接口清单。