JeecgBoot v3.9.1 多漏洞审计教学文档
前言
本教学文档旨在详细剖析 JeecgBoot v3.9.1 版本中存在的多个安全漏洞。JeecgBoot 是一个在 GitHub 上拥有 41k Star 的低代码开发平台,广泛应用于国内企业。本次审计主要针对其新增的 AIRAG 模块和已进行过安全修复的字典接口,共发现两类严重漏洞:AIRAG MCP 模块的命令执行漏洞和字典接口的 SQL 注入漏洞(绕过了历史修复方案)。文档将深入讲解漏洞成因、利用条件、挖掘过程及技术细节,以供安全研究、代码审计和防御加固参考。
项目背景与技术栈
- 项目名称:JeecgBoot
- 版本:v3.9.1
- 技术架构:标准 Spring Boot 结构,后端业务基于 Spring MVC,持久层使用 MyBatis。
- 新增模块:v3.9.1 版本引入了 AIRAG 模块,该模块支持 MCP (Model Context Protocol) 协议,用于工具调用和进程间通信。
- 历史安全背景:项目的字典相关接口曾存在 SQL 注入漏洞,官方在 v3.4.x 版本中采用了黑名单过滤方案进行修复。
漏洞一:AIRAG MCP 命令执行漏洞 (CVE-2026-xxxxx)
漏洞描述
在 AIRAG 模块的 saveAndSync 接口中,用户可控的 endpoint 字段在 stdio 模式下被未经任何过滤直接拼接进 sh -c 命令中执行,导致已认证的攻击者可实现远程命令执行 (RCE)。
前置条件
- 身份认证:攻击者需拥有一个有效的已认证用户会话。
- 配置开启:目标系统的配置文件 (
application.yml等) 中,jeecg.ai-rag.allow-sensitive-nodes配置项必须包含stdio。该配置在开发和演示环境中默认开启。
漏洞挖掘与分析过程
- 目标定位:审计时首先关注新增模块
jeecg-boot-module-airag。由于 MCP 协议涉及工具调用和进程通信,此类模块是安全审计的天然重点。 - 漏洞接口:在
AiragMcpController.java中发现了saveAndSync接口。该接口逻辑为:先调用edit方法保存实体,再调用sync方法进行同步。 - 实体分析:查看
AiragMcp实体类,其type和endpoint字段均通过@RequestBody接收,完全用户可控。注释明确说明 “stdio类型为命令”。 - 漏洞触发点:跟进
AiragMcpServiceImpl.java中的sync方法。在type为"stdio"的分支中,存在以下关键代码:
攻击者控制的// endpoint.trim() 直接拼接入命令数组 String[] cmdParts = new String[]{"sh", "-c", endpoint.trim()};endpoint字段在去除首尾空格后,直接被放入ProcessBuilder的命令参数中,最终由/bin/sh -c执行,中间零过滤。 - 配置检查:代码中有一处
allowSensitiveNodes配置检查,但默认配置allow-sensitive-nodes: sql,stdio即已包含stdio,因此该检查在默认环境下不构成防御。
漏洞利用调用链
POST /jeecg-boot/airag/airagMcp/saveAndSyncAiragMcpController.saveAndSync(@RequestBody AiragMcp)airagMcpService.edit(airagMcp)// 将包含恶意命令的endpoint持久化到数据库airagMcpService.sync(id)cmdParts = ["sh", "-c", endpoint.trim()]StdioMcpTransport→ProcessBuilder→ 执行/bin/sh -c "<恶意命令>"
修复建议
- 对
endpoint字段进行严格的输入校验和白名单控制,仅允许特定的、安全的命令或参数。 - 避免将用户输入直接拼接进操作系统命令。如必须执行命令,应使用参数化传递,并对参数进行转义。
- 考虑在正式生产环境中关闭
stdio等敏感节点的默认启用。
漏洞二:SQL注入漏洞(绕过历史修复)
漏洞背景
JeecgBoot 的字典接口此前已曝出 SQL 注入漏洞。官方在 v3.4.x 版本中通过 SqlInjectionUtil.specialFilterContentForDictSql() 方法引入了黑名单过滤方案进行修复。本次审计发现,该黑名单存在绕过方法,并且部分接口甚至完全未应用此过滤。
历史防护机制分析
首先分析黑名单的实现 (SqlInjectionUtil.java):
- 关键词黑名单:包含
select(注意后面有空格)、union、information_schema等。关键缺陷在于:select后必须跟空格才被拦截。information_schema需完整匹配。- 未拦截
exists、and、or等关键词。
- 正则表达式黑名单:包含
chr\s*\(、sleep\s*\(、user\s*\(等。关键缺陷在于:user\s*\(匹配的是user()带括号的形式。- 同样存在对空格 (
\s*) 的依赖。
黑名单绕过手法
基于上述分析,可构造以下绕过Payload:
- 绕过
select:使用%0a(换行符) 或%0d(回车符) 等空白字符替代空格。例如:select%0a1。 - 绕过
user():使用无需括号的 MySQL 内置函数或变量,如current_user、user(不带括号)、@@version。 - 绕过
information_schema:使用 MySQL 中其他包含元数据的系统表,如mysql.innodb_table_stats、sys.schema_auto_increment_columns。 - 利用未过滤关键词:直接使用
and、or、exists等未被拦截的关键词进行布尔逻辑判断。
漏洞接口详情汇总
审计发现多处接口存在 SQL 注入,根因在于 MyBatis 映射文件 (SysDictMapper.xml) 中直接使用 ${} 进行字符串拼接,而非安全的 #{} 参数化查询。主要分为两类:应用了可绕过的黑名单过滤 和 未应用任何过滤。
下表汇总了部分存在漏洞的接口:
| 接口路径 | 注入参数 | 注入类型 | 是否过黑名单 | 利用要点 |
|---|---|---|---|---|
/sys/dict/getDictItems/{dictCode} |
filterSql |
布尔盲注 | 是 (可绕过) | dictCode 以逗号分割,第4部分作为 filterSql 传入。 |
/sys/dict/loadDict/{dictCode} |
keyword |
LIKE注入 | 否 | keyword 直接拼接入 LIKE 语句,无任何过滤。 |
/sys/dict/loadTreeData |
_tableFilterSql (在condition JSON中) |
时间盲注 | 否 | condition JSON 中的 _tableFilterSql 值直接透传至 ${value}。 |
/sys/api/queryFilterTableDictInfo |
filterSql |
布尔盲注 | 是 (可绕过) | 同第一类接口。 |
/sys/api/queryTableDictItemsByCode |
tableFilterSql |
布尔盲注 | 是 (可绕过) | 同第一类接口。 |
/sys/api/loadDictItemByKeyword |
keyword |
LIKE注入 | 否 | 同第二类接口。 |
/sys/dict/loadDictItem/{dictCode} |
dictCode (作为表名) |
布尔盲注 | 是 (可绕过) | 表名直接拼接。 |
典型漏洞代码分析
案例1:/sys/dict/getDictItems/{dictCode} (可绕过的黑名单)
- Controller层 (
SysDictController.java):将dictCode按逗号分割,第4部分作为filterSql传入 Service 层。 - Service层 (
SysDictServiceImpl.java):对filterSql调用SqlInjectionUtil.specialFilterContentForDictSql(filterSql)进行过滤(可绕过)。 - Mapper层 (
SysDictMapper.xml):过滤后的filterSql直接通过${filterSql}拼接进 SQL 语句。 - 利用Payload示例:
- 正常请求:
/getDictItems/sys_user,realname,username,1=1返回数据。 - 异常请求:
/getDictItems/sys_user,realname,username,1=2返回空,构成布尔盲注。
- 正常请求:
案例2:/sys/dict/loadDict/{dictCode} (未过黑名单)
- Service层 (
SysDictServiceImpl.java):keyword参数未经任何过滤,直接拼接进LIKE语句:keywordSql = "("+text + " like '%"+keyword+"%' or "+ code + " like '%"+keyword+"%')"; - 利用Payload示例:
keyword=%' OR 1=1 OR '%'='- 拼接后SQL:
... WHERE (realname like '%%' OR 1=1 OR '%'='%' ...),LIKE条件被绕过,返回所有数据。
- 拼接后SQL:
案例3:/sys/dict/loadTreeData (JSON透传,未过黑名单)
- Service层:
condition参数为 JSON 字符串,解析后直接放入Map,其中的_tableFilterSql键值对直接透传到 Mapper。 - Mapper层:通过
${value}直接拼接,无任何过滤。 - 利用Payload示例:
condition={"_tableFilterSql":"1=1 and sleep(5)"}- 请求耗时从基线的 ~0.016秒 变为 ~15秒,构成时间盲注。
修复建议
- 根本性修复:将所有 MyBatis 映射文件中的
${}动态拼接替换为#{}预编译(参数化查询)。这是解决 SQL 注入最彻底的方法。 - 弃用黑名单:黑名单过滤方案存在固有的被绕过风险,不应作为主要防御手段,仅可作为辅助的深度防御措施。
- 输入校验:对所有用户输入的参数进行严格的类型、格式、长度和业务逻辑校验。
- 最小权限原则:数据库连接账户应遵循最小权限原则,避免使用高权限账号运行应用。
总结
本次对 JeecgBoot v3.9.1 的审计揭示了两类高危漏洞:
- AIRAG MCP 命令执行漏洞:源于新引入模块对用户输入 (
endpoint) 的极度不信任处理,直接拼接进 Shell 命令。利用门槛低(已认证+默认配置),危害极高。 - SQL注入漏洞集群:根源在于持久层框架(MyBatis)错误地使用了
${}进行 SQL 拼接。历史修复采用的黑名单方案存在明显缺陷,可通过换行符、无括号函数、替代系统表等方式绕过。更严重的是,部分接口完全跳过了该黑名单过滤,导致直接可注入。
这两类漏洞的共性在于:对用户输入缺乏充分的校验和安全的处理方式。开发者在引入新功能(MCP)和修复旧漏洞时,都未能严格遵守安全编码规范,导致了严重的安全隐患。这提醒我们,安全修复必须触及根源(如将动态拼接改为参数化查询),并需进行完整的回归测试,避免修复不彻底或引入新问题。