Skip to content
Kurisu
Go back

JeecgBoot 双防线 SQLi 绕过实战:关键词黑名单 + Druid Wall 下的布尔盲注

前段时间白盒审计一套基于 JeecgBoot 2.4.5 的二开系统,在框架自带的查重接口里挖到一个 SQL 注入。这个接口前面站着两道防线——应用层关键词黑名单和 Druid WallFilter——单独看都不算结实,叠在一起倒是把常规 payload 全废了。这篇记录完整的绕过过程,目标信息全部略去,只留技术本身。

漏洞点:框架自带的查重接口

JeecgBoot 自带一个 DuplicateCheckController,做”字段值是否重复”的查重——就是注册页上”该用户名已被占用”那种提示背后的接口。问题在它的 SQL:

SELECT COUNT(*) FROM ${tableName} WHERE ${fieldName} = #{fieldVal}

tableNamefieldName 都是 ${} 直拼。fieldVal 倒是 #{} 预编译——但没什么用,前面两个坑够大了。

审计二开系统的时候别光盯着业务代码,基座框架自带的 controller 要一个个过,这种地方经常有惊喜。

防线一:应用层关键词黑名单

请求先进 SqlInjectionUtil 这类工具类过一道黑名单。读代码发现它的规则有个毛病:关键词都带着尾随空格,比如 select or from

这意味着:

就算个别规则是整词匹配,也可以用 /**/ 把词隔断。不过这场合连隔断都用不上——无空格形态已经全绕了。

这个设计失误挺典型:写黑名单的人按”人写 SQL 的习惯”想关键词,忘了 SQL 语法里空格可以换成括号、注释、换行一堆东西。

防线二:Druid WallFilter

过了应用黑名单,SQL 还要进 Druid 的 WallFilter。这套配置里禁了注释——/**/--# 全杀。

所以最终 payload 的形态必须是:零注释、零黑名单词的完全合法 SQL。约束听着苛刻,其实 MySQL 的合法语法空间够大:

盲注构造

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,直接登录后台完成接管。

小结

这个洞两个值得记的点:

  1. 按”带空格的关键词”写黑名单等于没写。SQL 里空格的可替代形态太多了——括号、注释、换行、版本注释。防御要么白名单(标识符只允许字母数字下划线),要么参数化,关键词黑名单是安慰剂。
  2. 业务响应差异本身就是盲注通道。这个接口连报错都不用构造,“可用/不可用”两个状态就是现成的布尔位。

修复也简单:${}#{}tableName/fieldName 走白名单校验,框架版本升到 3.5+。


Share this post:

Previous Post
SRC 漏洞提交实战笔记:EDUSRC、CNVD 的规则差与坑
Next Post
LNMP 源码编译与 DVWA 靶场部署:一堂课踩完的所有坑