Java 代码审计实战速查(一):依赖与配置

拿到一套 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.2Padding Oracle(Shiro-721)
Shiro≤ 1.8.0多个路径匹配导致的权限绕过
Fastjson≤ 1.2.80AutoType 多轮绕过,最终演变成通杀
Jackson-databind2.9.x 及以下高危集中开启 defaultTyping 后反序列化 RCE
Log4j22.0-beta9 ~ 2.14.1Log4Shell(CVE-2021-44228)
Struts22.3 ~ 2.3.34、2.5 ~ 2.5.16OGNL 表达式注入 RCE(S2-045 / S2-057 等)
Commons-Collections3.0 ~ 3.2.1反序列化利用链的弹药库
Commons-Beanutils< 1.9.4反序列化利用链
XStream< 1.4.18反序列化 RCE
SnakeYAML< 2.0Yaml.load() 反序列化(CVE-2022-1471)
POI< 3.11XXE / DoS
Spring Framework< 5.3.18、< 5.2.20Spring4Shell(CVE-2022-22965)
Spring Cloud Gateway< 3.1.0、< 3.0.6Actuator 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 的鉴权状态

产出:一张"重点跟进清单",后面进代码阶段按这张单子走


下一篇进代码,讲怎么把几百个类收敛成一张能用的接口清单。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇