实战|过滤了一堆注入关键词,我用一道排除法把库名拖了出来

admin 2026-06-30 09:09:04 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文复盘某教育系统登录口SQL注入实战,目标采用关键词过滤拦截常规sleep、updatexml等payload。作者通过单引号转义三步法确认注入点,利用casewhen配合SQLServer除零报错构建布尔盲注信道,通过302/500状态码差异逐位获取库名。文章详细记录了信道建立、字符二分法猜测、踩坑点(URL编码问题)及修复建议(参数化查询、密码哈希、最小权限等),展示了绕过黑名单的完整盲注流程。 综合评分: 88 文章分类: web安全,渗透测试,漏洞分析,实战经验,安全开发


cover_image

实战 | 过滤了一堆注入关键词,我用一道排除法把库名拖了出来

原创

狗头安全 狗头安全

狗头网络安全

2026年6月29日 20:06 四川

在小说阅读器读本章

去阅读

某edu登录口sql注入

0x00 前言

这篇文章给师傅们复盘一个最近测的登录注入,目标已脱敏,接口统一写成 /xxx,IP、单位、库名、账号也都打码了。漏洞本身不算多高深,但有两个点我觉得值得拿出来说说:

  1. 这站做了关键词过滤sleepupdatexmlextractvalue 这些常规 payload 一发过去就 500,一开始我差点以为是没洞。
  2. 绕过用的不是什么花活,就是一个标准的 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

CASEWHENTHEN1/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 的逐段判断能拼出版本。再往下顺着系统表(sysobjectssyscolumns 这些)遍历,表名、列名、任意一行数据,理论上都能逐字段读出来——包括读者信息、借阅记录、登录表里的账号密码。

0x07 顺手记几个雷

复盘时还发现几个一并写进报告的点:

  • 密码大概率明文存。pass

    是原样拼进 SQL 去比对的,说明库里存的就是明文。一旦被拖库,全站密码连个哈希都没有,直接裸奔。

  • 存在升级成 RCE 的可能。

    如果这个数据库连接账号权限给大了(比如 sysadmin),SQL Server 还能上 xp_cmdshell 执行系统命令,那性质就从”数据泄露”变成”服务器失陷”了。这条我没实际打,报告里标的是”潜在风险,未验证”——师傅们做授权测试,没明确授权的高危动作千万别手贱。

  • 同系统多点部署。

    同一套程序挂在好几个地址上,一个洞,几台一起中招。

0x08 修复建议

给开发同学的整改清单,按优先级排:

  1. 参数化查询,这是唯一治本的。

    把拼接 SQL 改成预编译参数,MyBatis 用 #{} 而不是 ${}。核心就一句:让用户输入永远只能当”数据”,没机会变成 SQL 的一部分。这一步做到位,上面所有花活全部失效。

  2. 密码别明文存。

    上 bcrypt / scrypt / Argon2,加盐哈希。

  3. 关键词黑名单只能当辅助,别当主防线。

    这次就是最好的反面教材:它拦了五个词,漏了 CASE WHEN,然后就被人从这个缝里钻进来了。黑名单永远在追着攻击者的写法跑,补不完的。

  4. 登录接口加限流 + 账号锁定。

    盲注动辄成百上千个请求,限速能把自动化攻击的成本顶得很高。

  5. 数据库账号按最小权限给。

    别动不动 sysadmin,把那条能升级到命令执行的路,在配置层就堵死。

0x09 小结

这站给我最大的感受是:开发其实安全意识,知道要防注入,也确实动手拦了一批关键词。可惜方向反了。

拦关键词,本质是在赌”攻击者只会用我见过的写法”。可注入的写法是写不完的,你拦 sleep,我用 CASE WHEN;你再拦 CASE,我还有别的。参数化查询不一样,它不跟你比谁的字典大,它直接让所有 payload 都落不了地。

一个是猜对方下一张牌出什么,一个是让对方根本上不了桌。差距就在这儿。


免责声明

本文所述测试均在合法授权范围内进行,文中涉及的目标单位、IP、接口路径、数据库信息均已脱敏处理,相关 payload 和细节仅用于安全研究与防御学习。请勿利用本文技术对任何未授权的系统进行测试,由此产生的一切后果与作者无关。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:狗头网络安全 狗头安全 狗头安全《实战 | 过滤了一堆注入关键词,我用一道排除法把库名拖了出来》

    深度资产信息查询工具 网络安全文章

    深度资产信息查询工具

    文章总结: 该文档为知树安全团队发布的深度资产信息查询工具介绍,主要内容为通过公众号回复特定数字代码获取免费安全资源,包括免杀课程、漏洞POC、爆破字典等各类网
    评论:0   参与:  0