news 2026/10/19 9:50:58

SQL注入进阶:盲注、堆叠注入与二次注入实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL注入进阶:盲注、堆叠注入与二次注入实战解析

不讲多余的话,接着上一篇继续。上一篇把联合查询、报错注入这类“有回显”的玩法聊透了,实战里也够用一阵子。但真到目标环境里,你会发现情况往往没那么理想:页面要么只回显“是”和“不是”,要么干脆什么都不显示,甚至你输入的引号都被过滤掉。这时候,SQL注入的第二阶段才算真正开始。

这篇文章不会去聊什么“从入门到放弃”,只聊三个进阶方向:盲注、堆叠注入、二次注入,外加一些绕过思路。每一部分我都会把原理、判断方法、实操语句、踩坑记录放在一起讲。老规矩,所有测试都在你自己搭的靶场环境里做,别拿别人的线上系统练手,这是底线。

1. 为什么会有盲注这种“笨办法”

1.1 有回显和没回显的处境差异

联合查询注入的核心前提是:页面会把你查询结果里的字段直接渲染出来,所以你可以通过“数列数→找显示位→拼查询”这条路一直走下去。报错注入也差不多,数据库会把报错信息返回到页面,你利用updatexml、extractvalue这些函数把数据“塞”进报错里带出来。

但实际的业务系统里,很多查询结果只是用来做逻辑判断,并不会把数据库内容展示给用户,比如:

  • 登录验证:查到了就跳转,查不到就提示用户不存在
  • 文章是否存在:存在就返回200,不存在返回404
  • 搜索功能:有关键词就显示结果列表,没有就显示空状态

这些场景的共同特征是:不返回数据内容,只返回“真”或“假”。盲注的思路就是利用这种二值判断,把SQL查询变成一个一个的“是非题”,通过逐字符比对拼出完整数据。而且盲注的适用范围其实比你想象的广,即使有回显的场景,在数据量比较大、需要脚本化提取或者某些特殊字符被过滤的时候,盲注往往也是退路。

1.2 盲注的分类和适用场景

盲注分三种,我建议按使用频率排列:

类型判断依据适用场景优缺点
布尔盲注页面内容/状态码变化页面能区分真假响应速度相对最快,但需要页面有明显差异
时间盲注响应时间延迟页面不区分真假响应适用面最广,但速度慢,容易被WAF拦截
报错盲注报错信息报错被隐藏但可被带出速度快,但依赖数据库配置

初学者最常见的困惑是:我怎么判断这个环境适不适合用盲注?我的经验是,先做最简单的测试。在参数后面加一个单引号,看页面是否有变化;再试and 1=1和and 1=2,对比页面差异。如果这两组请求的响应有肉眼可见的区别,那恭喜,布尔盲注直接上手。如果完全没区别,再考虑时间盲注。

但这里有个细节很多人会忽略:页面差异不一定只是文字内容,也可能是响应码、响应头、重定向位置、HTTP状态码,甚至是一个异步请求的返回结果。我遇到过好几次,主页面完全不变,但多了一个JavaScript发起的子请求,那个子请求才暴露了真假的区别。所以判断差异的时候,多注意浏览器开发者工具的网络面板,而不是只看页面。

1.3 一个容易混淆的概念:盲注不是低效的借口

很多新手觉得盲注麻烦、效率低,宁愿去爆破参数、试工具也不愿意手动测试。实际上盲注在真实场景中的出场率非常高,因为很多开发者在写代码的时候根本没考虑过输出用户可控数据的问题,但在“是否返回结果”这件事上又做得比较规矩。加上现在很多WAF对明显的联合查询和报错注入拦截得很死,盲注这种“看起来只是访问了一下页面”的请求反而容易过关。

所以盲注不是最后的手段,而是渗透测试中必须熟练掌握的基础技能。工具如sqlmap确实能自动化大部分盲注过程,但如果你不理解背后的原理,遇到工具跑不出来的情况,比如基于JSONP回调的盲注、需要带Token的请求、或者数据库字符集设置有问题导致中文乱码时,你根本不知道问题出在哪里。

2. 布尔盲注与时间盲注:从原理到实践

2.1 布尔盲注的核心判断逻辑

布尔盲注的本质是:构造一个SQL表达式,让它在“真”和“假”两种情况下产生可区分的页面响应。最基础的测试思路是这样的:

?id=1 and 1=1 -- - -- 正常页面 ?id=1 and 1=2 -- - -- 非正常页面(可能空白、报错、404)

如果这两条请求的页面有明显的状态差异——比如内容有无、字数多少、元素是否加载——就可以继续往下测。接下来的思路是猜解数据库中的数据,核心函数是substr()和ascii():

?id=1 and ascii(substr(database(),1,1))>100 -- - -- 返回正常,说明第一个字符的ASCII大于100 ?id=1 and ascii(substr(database(),1,1))>120 -- - -- 返回正常,继续猜 ?id=1 and ascii(substr(database(),1,1))=119 -- - -- 等于119,即字符'w'

这里解释一下为什么要用ascii()而不是直接substr(database(),1,1)='w'。因为布尔盲注的判断条件是一个一个拼出来的,直接比较字符串在引号处理上很麻烦,而且遇到过滤引号的场景就直接废了。用数字比较就清爽得多,也容易写成脚本去跑。

实际写脚本的时候,推荐用二分法而不是逐一遍历。二分法就是每次猜范围的一半,比如先判断是否大于64,再判断是否大于96,这样每个字符最多用7到8次请求就能确定,而不必一个ASCII码一个ASCII码去试256次。

2.2 布尔盲注的实操步骤和去重技巧

直接给出一个可复用的手工测试流程:

  1. 在参数后加',观察页面异常情况
  2. 用and 1=1和and 1=2对比确认注入点
  3. 用order by N确定结果集列数(虽然盲注不需要联合查询,但这一步能判断注入点闭合方式)
  4. 猜数据库名长度:and length(database())=N
  5. 逐字符猜解数据库名
  6. 猜表名长度、表名第一个字符、第二个字符……以此类推
  7. 猜列名、最后猜数据

这里有一个必须强调的点:布尔盲注的关键不是“能不能猜出来”,而是“你怎么知道猜对了”。在手工测试的时候,一定要先截取一个“真”页面的响应包,再截取一个“假”页面的响应包,用对比工具(Burp的Comparer或者任何diff工具)确认它们的差异点在哪里。很多时候差异不在正文里,而在一个看不见的隐藏字段、一个JS变量、或者一个HTTP头部。

我在一次自建靶场测试里遇到过这种情况:页面正文完全一样,但真响应里多了个Set-Cookie: flag=1的头部,假响应里是Set-Cookie: flag=0。那一次如果用页面内容做判断依据,就什么都测不出来。所以做盲注的时候,把响应包完整看一遍,比盯着页面看有用得多。

2.3 时间盲注:没有回显,那就让数据库说话

当页面真假响应完全不可区分时,布尔盲注就失效了。这时候需要让数据库主动产生一个可控的信号——响应时间的延迟。时间盲注的核心函数是sleep()或者benchmark(),和条件判断组合起来:

?id=1 and if(ascii(substr(database(),1,1))=119,sleep(3),0) -- -

执行逻辑:如果database()的第一个字符的ASCII等于119,就sleep(3),页面会卡顿3秒左右再响应;否则立即返回。这样你就可以通过“是否卡顿”来确认条件真假。

这个方案的问题也很明显:一个字符用二分法可能几十个请求,每个请求最坏情况要等3秒,整个库猜下来可能需要几个小时。所以实操里有几个优化技巧:

  • 缩短延时到1秒或1.5秒,够你分辨就行,不要用5秒那么夸张
  • 用benchmark(10000000,md5(1))替代sleep(),在sleep被禁用的时候可以用这个方式凑出延迟,但要注意benchmark会打满CPU,并发太高可能把数据库搞挂
  • 尽量用基于重定向或异步请求的外部带出方式,比如把数据拼进DNS请求里,但这个属于进阶玩法,后面有机会细说

时间盲注有一个比较隐蔽的坑:数据库连接池和页面缓存会让延迟判断失真。比如你第一次请求因为缓存走了CDN节点,延迟只有0.3秒,第二次请求回源了,延迟3秒,你就可能把“假”误判为“真”。所以在做时间盲注前,先发几个不带sleep的请求测试基线延迟,然后每次请求之间保持均匀间隔,不要忽快忽慢。

2.4 盲注脚本编写思路:别只会用工具

sqlmap当然能跑盲注,但是自己写一次脚本会让你对盲注的理解上一个台阶。核心逻辑就是一个递归或循环的二分猜测,我用一个简单伪代码说明整个流程:

import requests def check_true(payload): url = "http://target.com/view.jsp?id=1" + payload resp = requests.get(url, timeout=10) return b"some_marker" in resp.content # 判断"真"的标记 def get_char(prefix, total_length): left, right = 32, 126 # 可打印字符的ASCII范围 while left < right: mid = (left + right) // 2 if check_true(f" and ascii(substr(({prefix}),{total_length},1))>{mid} -- -"): left = mid + 1 else: right = mid return chr(left)

当然实际写脚本时你还需要考虑请求头、会话、编码、二分边界、WAF参数变形等问题,但骨架就是这个样子。一次完整的数据库名提取就是先length(database())得到长度,再用上面函数循环拼接。

3. 堆叠注入:被很多人忽略的多语句执行能力

3.1 堆叠注入的原理和触发条件

堆叠注入(Stacked Injection)是指在一个数据库调用中执行多条SQL语句的能力。一般的SQL注入利用的是查询语句内部的逻辑变形,比如SELECT * FROM news WHERE id = 1 AND 1=2,这始终是一条SELECT语句。而堆叠注入通过分号(;)把多条语句串联起来:

?id=1; DROP TABLE test; ?id=1; SELECT username,password FROM users;

原理层面很简单:数据库原生API(比如PHP的mysqli_multi_query()、Python的某些驱动)允许在一个调用里执行多个以分号分隔的语句,应用程序没有对这种行为做限制,于是攻击者就可以在这里插入任意的增删改语句。

但堆叠注入有一个前置条件经常被忽略:数据库连接驱动必须支持多语句执行,而大多数标准查询接口(比如mysqli_query()配合prepare)是不支持多语句的,一条SQL发送给数据库,执行完就返回了。所以碰到堆叠注入的情况,其实说明开发者在某种场景下主动使用了多语句执行接口,比如批量更新、批量插入这些功能。

3.2 什么时候用堆叠注入

堆叠注入和前面说的盲注、联合查询不一样,它的意义在于“能做事”,而不仅仅是“能取数”。以下这些操作,单靠SELECT注入是做不到的:

操作堆叠注入能否实现仅靠SELECT注入能否实现
插入恶意数据(二次注入铺垫)能不能
修改数据(比如改自己的权限)能不能
删除数据能不能
调用存储过程能部分场景能
查询其它库的表数据能跨库查询可能需要配置

我举一个实际场景:假设系统的用户表里有一个用来“记住我”的字段,存的是用户输入的昵称,它会在管理员后台被无过滤地拼接进SQL。那攻击者可以先在注册或者修改资料的地方写入一条带注入负载的数据,这个负载设计成“把管理员密码重置成指定值”,然后在后台触发这条SQL时,就通过堆叠注入完成了权限变更。

这里要注意,堆叠注入成功与否,还取决于后端语句的拼接方式。如果目标位置是用参数化查询的预编译语句,那分号也只会被当作普通字符串处理,怎么注入都是无效的。所以测试堆叠注入的第一步,永远是先试最简单的;或者; -- -,看是否会报语法错误。

3.3 堆叠注入的实操案例和限制

可以先在靶场里构造一个最简单的场景,比如有一个search.php?id=1的页面,后端代码用了多语句查询:

$conn->multi_query("SELECT * FROM products WHERE id = " . $_GET['id']);

这时候输入; DROP TABLE IF EXISTS products; -- -,如果这条语句执行成功,页面会显示查询结果不存在,而数据库里的表已经被删除了。实际操作用来时:

?id=1; INSERT INTO admin(username,password,level) VALUES('temp','p@ss123',1); -- -

这是在不知道后台密码的情况下,直接向管理员表插入一条自己可控的记录,之后就能用这条记录登录后台。注意表结构你得提前通过其它方式拿到,比如联合查询把admin表的字段数、字段名先确认好,再执行插入。

这个技巧的关键是:堆叠注入的执行结果通常不反馈在页面上,所以你能不能看到效果,取决于后续会不会使用到你插入的数据。也因此,堆叠注入经常和“二次注入”配合使用。

4. 二次注入与绕过思路:进阶路上的两道坎

4.1 二次注入:存储型漏洞的反直觉之处

二次注入的独特性在于:注入负载第一次被写入数据库时,因为数据处理过程使用了转义或参数化查询,所以没有立刻生效,反而被当作普通字符串“安全地”存储了下来。真正触发漏洞的,是后续某个功能在读取这条数据并拼接成SQL语句的那一刻。

我举个例子帮你理解整个过程。假设注册功能把用户名经过转义后存入数据库,输入admin' AND 1=1 -- -,这里转义让单引号变成了普通字符,所以第一条SQL安全执行,数据正常入库。但数据库里存的就是admin' AND 1=1 -- -这个原样字符串。过几天管理员在后台做一个重置密码的功能,代码这么写:

$sql = "UPDATE users SET password = 'newpass' WHERE username = '" . $user . "'";

此时$user是从数据库里读出来的用户名字段,没有二次处理就拼进SQL。如果这个用户名字段的值就是admin' OR 1=1 -- -,那实际执行的SQL变成:

UPDATE users SET password = 'newpass' WHERE username = 'admin' OR 1=1 -- -'

单引号闭合成功,OR 1=1让整张表所有用户的密码都被重置。这个场景是最典型的二次注入。所以测试二次注入的思路和普通注入完全不一样:你需要先找“写入点”,再找“使用点”,然后把负载植入到写入点。

实际操作中建议构造两类负载:一类不影响自己这行的正常功能,避免被发现;另一类利用拼接点做逻辑扩展,比如字符串闭合后加条件、子查询、多语句等。在靶场测试时,可以专门做一个“用户名”字段,写入test' AND updatexml(1,concat(0x7e,(SELECT password FROM users WHERE username='admin'),0x7e),1) -- -,模拟的就是“管理员或系统读取用户名并拼入某个查询时,触发报错注入”。

4.2 绕过WAF与过滤:理解拦截逻辑,而非死记技巧

绕过手段是每个做渗透的人都会接触到的话题,但它非常容易走偏。很多初学者拿到一堆绕过payload就开始背,却完全不理解每条绕过语句背后的拦截逻辑。我劝你一句:绕过技巧会过时,但“理解拦截规则然后找逻辑缺口”的能力永远不会过时。

从需求反推拦截方做了什么,常见情况无非三种:

  • 字符过滤:把'、"、--、#等特殊符号去掉或转义
  • 关键字过滤:把union、select、sleep等SQL关键字替换为空或拦截
  • 语义识别:通过解析SQL语法判断请求中是否存在“危险逻辑”

对应的绕过思路分别是等价替换、编码变形、注释切分。比如:

-- 简单关键字过滤 union select -> un/**/ion sel/**/ect -- 引号被过滤 用十六进制替代字符串,如'admin' -> 0x61646d696e -- 空格被过滤 用tab(%09)、注释符/**/代替空格 -- 报错函数被拦截 updatexml被拦截时,用floor(rand(0)*2)这类旧式报错

这些技巧本身不复杂,关键是你要能搞清楚拦截点在哪一层。如果过滤器只做了字符串替换,ununionion这种写法可以绕过——因为替换后剩下的还是union;如果过滤器做了正则匹配,/**/这类注释符有时能被解析成空格,有时不会。我建议你在本地先搭一个简单的过滤脚本,把常见的过滤正则写进去,逐个test一下,你就明白哪些变形真正有效,哪些只是心理安慰。

4.3 一个提醒:绕过的前提是合法授权

正因为上面这些绕过技巧太直接、太容易造成数据破坏,我需要把话再说重一点。所有注入测试都必须建立在你拥有目标系统的明确授权之上,这个授权可以是自己搭建的靶场,可以是CTF题目,可以是客户签署了渗透测试合同的系统。无授权做这些尝试,哪怕“只是看看能不能注入”,在法律上也不是“没有造成破坏就没问题”,而是“未经授权访问计算机信息系统”的行为。特别是二次注入和堆叠注入这种可能会修改数据的玩法,一旦造成数据丢失或者业务中断,后果完全不是一句“我试试”能带过的。

我自己的习惯是,每做一次测试之前,先明确回答三个问题:这个系统有没有书面或者邮件确认的授权?我接下来要做的操作是否在授权范围内?如果操作产生了异常,是否有回滚预案?三个问题只要有一个答案是否定的,那这次测试就不该继续。

5. 常见问题与排查技巧实录

5.1 注入点存在但联合查询一直失败是什么原因

面对这种情况,我的排查顺序一般是:

  1. 排除参数类型问题,有些参数是数值类型,不需要闭合引号,有些是字符串类型,需要先闭合,判断方法是用1和1'分别测试
  2. 确认前端代码是否存在参数过滤,比如Java的Integer.parseInt()就不会接收含引号的参数,这时候换个入口试
  3. 确认注入点在哪个SQL语句上,有的页面同时查询了多个地方,比如先查用户信息再查文章列表,注入点可能在后面的查询里,前面的返回结果掩盖了差异
  4. 开启SQL日志或者数据库的general_log,直接看后端实际执行的SQL语句,这个在自建环境里排查是最快的

5.2 为什么注入语句在数据库客户端能执行、在浏览器里就不行

最常见的两个原因:编码和特殊字符被URL编码。浏览器发送请求的时候,空格会被编码成%20或+,#会被当成锚点直接丢弃(实际上#后的内容根本不会发给服务器),特殊字符如'在一些情况下会被表单过滤。解决方法是把整个payload用URL编码后再发送,或者用Burp Suite的Repeater工具直接构造原始请求包,绕过浏览器的自动编码行为。另一个隐藏原因是字符集,如果页面和数据库的字符集不一致,传入的中文会被转成乱码,比如你在盲注里用substr对比中文字符时,可能需要对目标字符集做Hex编码而不是直接传原始中文。

5.3 盲注脚本总是误判,怎么提高准确度

我在写盲注脚本时吃过不少亏,总结下来有几个经验值得参考:

  • 不要用“包含某个字符串”做判断,优先用响应长度、响应码、关键Header这些更稳定的特征
  • 对同一个条件做两次或三次请求,取多数结果,降低网络抖动带来的误判
  • 二分法的时候把边界值多测一次,比如判断大于100为真、大于120为假,但等于101到119之间的值可能因为逻辑漏洞被漏掉
  • 在使用时间盲注时,用本地基准响应时间动态调整阈值,比如先检测五次正常请求的平均延迟,大于平均延迟的1.5倍才算“真”

5.4 堆叠注入遇到分号被过滤还能不能继续

分号被过滤确实会让堆叠注入的大多数玩法失效,但也不是完全没有办法。有些数据库允许在特定上下文里不使用分号分隔语句,比如存储过程里多条语句可以用换行分隔;或者你的SQL语句已经位于一个完整的语句末尾,这时后面不需要分号也能执行。比如:

?id=1; DROP TABLE test -- -

如果过滤器只是把;去掉,但保留后面的内容,可能会因为拼接不当产生语法错误,但某些情况下比如数据库把分号当普通字符忽略,语句依然能执行。另外,Content-Type和请求参数中不同位置的编码解码顺序不同,也可能让分号“绕”过字符串层面的过滤。但这些都是细节,我只建议在靶场环境里尝试这些技巧,生产环境这么搞风险太大。

6. 写在最后的实操建议

关于SQL注入的进阶玩法,上面聊到堆叠、二次注入、绕过这些概念时,我反复强调的都是“理解原理比背payload重要”。这句话听着像正确的废话,但它真的是我在无数次踩坑之后最深的体会。比如有一次我在测试一个时间盲注,用sqlmap怎么跑都跑不出结果,最后手工测试发现目标环境的sleep()函数根本不存在——是数据库的一个精简版分支。如果我只是盲目依赖工具,永远不会发现这个环境差异。后来我换成benchmark()和get_lock()函数,才成功拿到数据。

所以我建议你按这个顺序去提升:先自己写一遍布尔盲注脚本,再写一遍时间盲注脚本,然后用注入工具跑同一个靶场,对比工具生成的payload和你的手工payload之间的差异,找出工具为什么更快、为什么更稳。这个过程比单纯刷几十个靶场题目的收获大得多。

后续的扩展方向还有很多,比如针对NoSQL注入、JSON注入、ORM框架注入等新场景的测试思路,又比如拿到数据库权限之后如何做内网横向移动,这些话题都需要单独开篇来聊。希望这篇文章能让你对盲注、堆叠注入、二次注入和绕过一个相对完整的认知,之后遇到没有回显的注入点时,第一反应不再是“这题没法做”,而是“先判断页面差异,再决定走布尔还是时间”。这就是从入门到进阶最关键的一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:49:29

Token + 数字签名:API接口安全认证与防篡改防重放完整方案

先说个我实际碰到的场景。某内部平台要把订单查询接口开放给合作方&#xff0c;上线第一周就出事了&#xff1a;有人拿到一个有效的Token&#xff0c;然后写脚本把请求里的订单号换掉&#xff0c;直接遍历了整个订单库&#xff1b;还有人把请求截下来反复提交&#xff0c;明明只…

作者头像 李华
网站建设 2026/10/19 9:48:28

网络路由实验全解析:从静态路由到RIP/OSPF配置与排错

网络路由实验&#xff08;计网实验prj11&#xff09;——这大概是计网系列实验里最让人又爱又恨的一个。爱的是它把你前面学过的IP地址、子网划分、数据链路层封装全部串了起来&#xff0c;做完之后你对“一个数据包到底怎么从一个网段跑到另一个网段”会有非常直观的认知&…

作者头像 李华
网站建设 2026/10/19 9:47:18

生产环境Linux故障排查:定位、命令、场景与复盘全解析

半夜两点四十&#xff0c;值班手机被报警震醒这种事&#xff0c;干运维的迟早都会碰上。屏幕上监控曲线像一把直插天花板的尖刀&#xff0c;生产机器一个小时前还能正常响应&#xff0c;现在 SSH 连上去敲个命令都要卡三秒。这种时候最考验人的不是命令背得多熟&#xff0c;而是…

作者头像 李华
网站建设 2026/10/19 9:46:58

open-code-review:面向工程落地的本地化AI代码审查CLI工具

1. 这不是又一个“AI代码审查”玩具&#xff1a;open-code-review 的真实定位与生存逻辑open-code-review 这个名字乍看平平无奇&#xff0c;甚至有点“开源即正义”的朴素感。但如果你最近刷过 GitHub Trending、Hacker News 或者国内的 V2EX、知乎技术区&#xff0c;会发现它…

作者头像 李华
网站建设 2026/10/19 9:46:57

Altera FPGA上Nios软核通过Avalon总线驱动I2C IP的调试指南

简介&#xff1a;这套资料面向ALTERA FPGA嵌入式开发者&#xff0c;提供了I2C主控制器IP的完整VHDL实现&#xff0c;并展示如何通过Avalon接口挂接到NIOS II软核处理器&#xff0c;适用于需要与传感器、存储器等外设通信的项目。I2C协议只需两根信号线即可实现全双工通信&#…

作者头像 李华