前段时间白盒审计一套基于 JeecgBoot 2.4.5 的二开系统,在框架自带的查重接口里挖到一个 SQL 注入。这个接口前面站着两道防线——应用层关键词黑名单和 Druid WallFilter——单独看都不算结实,叠在一起倒是把常规 payload 全废了。这篇记录完整的绕过过程,目标信息全部略去,只留技术本身。
漏洞点:框架自带的查重接口
JeecgBoot 自带一个 DuplicateCheckController,做”字段值是否重复”的查重——就是注册页上”该用户名已被占用”那种提示背后的接口。问题在它的 SQL:
SELECT COUNT(*) FROM ${tableName} WHERE ${fieldName} = #{fieldVal}
tableName 和 fieldName 都是 ${} 直拼。fieldVal 倒是 #{} 预编译——但没什么用,前面两个坑够大了。
审计二开系统的时候别光盯着业务代码,基座框架自带的 controller 要一个个过,这种地方经常有惊喜。
防线一:应用层关键词黑名单
请求先进 SqlInjectionUtil 这类工具类过一道黑名单。读代码发现它的规则有个毛病:关键词都带着尾随空格,比如 select 、or 、from 。
这意味着:
select(password)——select 后面直接跟括号,没有空格,黑名单匹配不上,放行||、&&这种符号形态压根不在名单里
就算个别规则是整词匹配,也可以用 /**/ 把词隔断。不过这场合连隔断都用不上——无空格形态已经全绕了。
这个设计失误挺典型:写黑名单的人按”人写 SQL 的习惯”想关键词,忘了 SQL 语法里空格可以换成括号、注释、换行一堆东西。
防线二:Druid WallFilter
过了应用黑名单,SQL 还要进 Druid 的 WallFilter。这套配置里禁了注释——/**/、--、# 全杀。
所以最终 payload 的形态必须是:零注释、零黑名单词的完全合法 SQL。约束听着苛刻,其实 MySQL 的合法语法空间够大:
- 字符串全用 hex 字面量代替:
0x78就是'x',连单引号都不用打 - 函数调用天然无空格:
if(...)、ascii(...)、substr(...) - 子查询拿括号包起来:
(select(password)from(sys_user)where(username)=0x61646d696e)
盲注构造
fieldName 的位置在 WHERE 左边,最顺手的打法是把它整个换成一个 if() 表达式:
fieldName=if(1,0x78,0x79)
原理:if(1,'x','y') 返回 'x',于是 WHERE 'x' = 'x'(fieldVal 传 x)成立,COUNT 非零,接口回”该值不可用,系统中已存在”;if(0,...) 返回 'y',条件不成立,回”该值可用”。业务响应本身就是布尔 oracle,连时间延迟都不用看:
if(1,0x78,0x79) → {"success":false,"message":"该值不可用,系统中已存在!"}
if(0,0x78,0x79) → {"success":true, "message":"该值可用!"}
然后就是标准的逐字节拖数据:
if(ascii(substr((select(password)from(sys_user)where(username)=0x61646d696e),1,1))>63,0x78,0x79)
一位一位二分,管理员的 password 密文和 salt 就全出来了。
收尾:PBE 离线碰撞
JeecgBoot 存的密码不是裸 MD5,是 PBEWithMD5AndDES(PBKDF1 派生密钥 + DES,迭代 1000 次),盐值就存在同一张表里。密文和盐都有了,本地写个脚本离线碰撞就行——字典第一位放 123456,一发入魂。
那个环境的管理员口令真就是 admin/123456。再配合图形验证码 OCR,直接登录后台完成接管。
小结
这个洞两个值得记的点:
- 按”带空格的关键词”写黑名单等于没写。SQL 里空格的可替代形态太多了——括号、注释、换行、版本注释。防御要么白名单(标识符只允许字母数字下划线),要么参数化,关键词黑名单是安慰剂。
- 业务响应差异本身就是盲注通道。这个接口连报错都不用构造,“可用/不可用”两个状态就是现成的布尔位。
修复也简单:${} 改 #{},tableName/fieldName 走白名单校验,框架版本升到 3.5+。