news 2026/10/16 10:49:34

SQL注入手工实战:从类型判断到盲注的完整流程与踩坑记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL注入手工实战:从类型判断到盲注的完整流程与踩坑记录

1. 从一次手工注入踩坑说起

SQL注入这个课题,我在不同阶段反复回炉过很多次。最早接触的时候,总觉得它无非就是单引号加or 1=1,工具一跑就完事。直到后来在一个模拟靶场里,被一个看似简单的数字型注入卡了整整一个下午,才意识到自己对注入的理解有多浅。那次经历让我重新捡起手工注入的完整流程,从判断注入类型、确定字段数、找显示位,到逐步提取库名、表名、字段名,最后拿到目标数据。整个过程走下来,比用工具自动跑一遍收获大得多。

这篇记录围绕的是SQL注入专项练习的完整思路。我会把每个环节的判断依据、操作意图、参数计算过程都摊开讲清楚,而不是只给一个最终payload。适合已经了解SQL基本语法、想系统梳理手工注入流程的读者,也适合做过一些题目但总是卡在中间步骤、不知道下一步该干什么的朋友。全文会涉及注入类型判断、联合查询注入、报错注入、布尔盲注、时间盲注这几大类,每一类都会给出可复现的操作步骤和我自己踩过的坑。

需要提前说明的是,以下所有操作均在本地搭建的模拟练习环境中完成,目的是理解漏洞原理、提升防御意识。任何未经授权的测试都是不合规的,这一点必须先摆正。

2. 注入类型判断:别急着上payload

2.1 为什么第一步永远是判断类型

很多人拿到一个带参数的URL,第一反应就是直接丢and 1=1和and 1=2看页面变化。这个思路没错,但前提是你得先搞清楚参数是数字型还是字符型,否则payload的构造方式完全不同。数字型注入不需要闭合引号,字符型注入则需要先闭合前面的引号再注释掉后面的内容。如果类型判断错了,后面的所有步骤都会跑偏。

判断类型的核心逻辑其实很简单:观察参数在SQL语句中的位置。假设后端拼接的语句是select * from users where id = $input,那$input就是数字型;如果是select * from users where name = '$input',那就是字符型。我们看不到源码,只能通过构造不同的输入来推断。

2.2 数字型与字符型的实操区分

先试?id=1 and 1=1和?id=1 and 1=2。如果前者页面正常、后者页面异常或内容变化,说明and被当作SQL语法执行了,大概率是数字型。如果两者页面都一样,可能是字符型,因为1 and 1=1整体被当成了字符串的一部分。

接着试?id=1' and '1'='1和?id=1' and '1'='2。如果前者正常、后者异常,说明单引号成功闭合了原语句,这是字符型注入的典型特征。这里有个细节:有些环境会对单引号做转义,返回500错误或者直接过滤,这时候就要考虑宽字节注入或者其他绕过方式。

我个人的习惯是先用?id=1'单独测一下,看是否报错。如果报错信息里出现了SQL语法错误的关键词,那基本可以确认存在字符型注入点,而且报错信息本身可能就是一个信息泄露渠道。

2.3 注释符的选择与踩坑

字符型注入闭合引号后,必须把后面的内容注释掉,否则原语句的剩余部分会导致语法错误。常用的注释符有三种:--(注意后面有个空格)、#、/**/。不同数据库对注释符的支持不一样,MySQL里--和#都可以,但--后面必须跟空格,这一点很容易被忽略。

我踩过的坑是:在URL里传--的时候,空格可能被浏览器或中间件处理掉,导致注释失效。解决办法是用--+或者%23(#的URL编码)。另外,如果参数后面还有其他拼接内容,#可能比--更稳妥,因为它不需要空格。

提示:判断注入类型时,每次只改变一个变量,其他参数保持不变。同时注意观察页面返回的状态码、内容长度、响应时间,这三个维度往往比页面文字更可靠。

3. 联合查询注入:字段数是绕不过去的坎

3.1 用order by确定字段数

确认注入类型之后,下一步是确定当前查询返回的字段数。最常用的方法是order by:?id=1' order by 1--+、order by 2、order by 3……直到页面报错或异常,最后一个正常的数字就是字段数。

这里有个经验:不要从1开始一个一个试,可以先试order by 10,如果报错再往小试,用二分法能省不少时间。比如先试10报错,再试5正常,再试7报错,再试6正常,那字段数就是6。当然,题目环境通常字段数不多,直接顺序试也无所谓。

order by报错的原理是:SQL引擎在执行排序时,会检查排序的列是否存在。如果指定的列序号超过了实际返回的列数,就会抛出错误。所以最后一个不报错的序号就是字段总数。

3.2 用union select找显示位

知道字段数后,构造?id=1' union select 1,2,3--+(假设字段数为3)。页面会显示查询结果,其中某些数字会出现在页面上,这些位置就是“显示位”,可以用来回显我们想要的数据。

如果页面没有显示任何数字,可能是原查询有结果,union查询的结果被覆盖了。这时候把前面的id=1改成一个不存在的值,比如id=-1或id=0,让原查询返回空,union的结果就能显示出来。

显示位不一定全部可用,有些位置可能被模板限制或者做了处理。我一般会先把所有显示位都试一遍,看哪些位置能完整回显字符串,哪些只能显示数字。能显示字符串的位置优先用来提取库名和表名。

3.3 提取库名、表名、字段名的完整链路

拿到显示位后,按以下顺序逐步提取信息:

  • 查当前数据库名:union select 1,database(),3--+
  • 查所有数据库:union select 1,group_concat(schema_name),3 from information_schema.schemata--+
  • 查指定库的表:union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='库名'--+
  • 查指定表的字段:union select 1,group_concat(column_name),3 from information_schema.columns where table_name='表名'--+
  • 查数据:union select 1,group_concat(字段名),3 from 库名.表名--+

group_concat默认有长度限制,通常是1024字节。如果结果被截断,可以用substring分段读取,或者用limit逐条读取。我遇到过表名特别多的情况,group_concat只显示了一部分,后来改用limit 0,1、limit 1,1逐条查才拿全。

注意:information_schema是MySQL自带的元数据数据库,不同数据库的元数据表名不一样。如果目标不是MySQL,需要换成对应的系统表,比如SQL Server的sysobjects、Oracle的all_tables。

4. 报错注入:没有显示位时的突破口

4.1 报错注入的适用场景

联合查询注入的前提是页面有显示位。如果页面不显示查询结果,只返回“正常”或“异常”两种状态,联合查询就无能为力了。这时候可以尝试报错注入:构造一个会让数据库主动抛出错误的语句,把想要的数据夹带在错误信息里返回。

报错注入的核心是利用数据库的某些函数,在参数中嵌入子查询,当函数执行出错时,错误信息会包含子查询的结果。MySQL中常用的报错函数有extractvalue、updatexml、floor等。

4.2 extractvalue与updatexml的实操

extractvalue的语法是extractvalue(xml_document, xpath_string)。当xpath_string格式不正确时,会报错并显示xpath_string的内容。构造payload:

?id=1' and extractvalue(1,concat(0x7e,(select database()),0x7e))--+

0x7e是~的十六进制,用来作为分隔符,方便在报错信息中定位数据。报错信息大概长这样:XPATH syntax error: '~库名~'。

updatexml的原理类似:

?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

这两个函数在MySQL 5.1以上版本可用,但在新版本中可能被限制。如果报错函数被过滤,可以尝试floor报错、exp报错等其他方式。

4.3 报错注入的长度限制与分段读取

报错注入有一个明显的限制:错误信息通常只能显示32个字符左右。如果database()的结果很短,一次就能拿到;但如果要查表名、字段名,往往需要分段读取。

分段读取用substring或mid函数:

?id=1' and extractvalue(1,concat(0x7e,substring((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,30),0x7e))--+

然后调整substring的起始位置,从1、31、61……依次读取,直到拼接出完整结果。这个过程比较繁琐,但熟练之后也就是改几个数字的事。

我个人的经验是:先用length()函数确认目标字符串的总长度,再计算需要分几段读取。比如表名总长度是85,每段读30个字符,那就需要读3段(1-30、31-60、61-85)。这样不会漏读也不会多读。

5. 盲注:没有回显也没有报错怎么办

5.1 布尔盲注的判断逻辑

如果页面既不显示查询结果,也不返回报错信息,只根据条件真假返回不同状态,那就是布尔盲注。典型场景是:?id=1 and 1=1页面正常,?id=1 and 1=2页面内容变少或提示“不存在”。

布尔盲注的核心思路是把“提取数据”转化为“判断真假”。比如要判断数据库名的第一个字符是不是a,可以构造:

?id=1' and substring(database(),1,1)='a'--+

如果页面正常,说明判断为真;如果页面异常,说明判断为假。通过遍历所有可能的字符,就能逐位猜出完整的数据。

5.2 用二分法加速字符猜测

逐个字符遍历效率太低。假设字符集是a-z0-9,每个字符最多猜36次,一个10位的库名就要猜360次。用二分法可以把次数降到log2(36)≈6次。

二分法的逻辑是:先判断字符的ASCII码是否大于某个中间值。比如判断substring(database(),1,1)的ASCII码是否大于109(m的ASCII码):

?id=1' and ascii(substring(database(),1,1))>109--+

如果为真,说明字符在m之后;如果为假,说明在m之前。每次把范围缩小一半,6次左右就能确定一个字符。

我一般会写一个简单的脚本来自动化这个过程,但手工操作时也有技巧:先确定字符的大致范围(比如小写字母、数字、下划线),再在这个范围内二分。不要一上来就猜特殊符号,那样范围太大。

5.3 时间盲注:最后的兜底手段

如果页面连真假状态都不区分,无论输入什么都返回一样的内容,那就只能靠时间盲注了。时间盲注利用的是数据库的延时函数,比如MySQL的sleep():

?id=1' and if(substring(database(),1,1)='a',sleep(3),0)--+

如果判断为真,页面会延迟3秒返回;如果为假,立即返回。通过测量响应时间,就能判断条件真假。

时间盲注的缺点是效率极低,每次判断都要等几秒。实际测试中,我会把sleep的时间设短一点(比如1秒),同时用二分法减少判断次数。另外要注意网络波动的影响,最好对同一个条件测两次,确认时间差异稳定。

提示:时间盲注对网络环境要求较高,如果目标服务器响应本身就不稳定,很容易误判。建议先用sleep(0)和sleep(5)对比一下基线响应时间,确认延时函数确实生效。

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

6.1 注入点判断的常见误区

现象可能原因排查思路
and 1=1和and 1=2页面一样参数是字符型,未闭合引号尝试加单引号闭合
加单引号后页面报错存在注入,但注释符不对换用--+或%23
order by一直正常字段数比预期多加大数字继续试,或用二分法
union查询无回显原查询有结果覆盖了union把id改为负数或不存在的值
报错函数无输出函数被过滤或版本不支持换其他报错函数或改用盲注

6.2 过滤绕过的几个实用技巧

有些环境会对关键字做过滤,比如把select、union、and替换成空字符串。常见的绕过方式:

  • 大小写混写:SeLeCt、UnIoN
  • 双写关键字:selselectect(过滤一次后剩下select)
  • 用注释分隔:sel/**/ect
  • 用等价符号替代:and换成&&,or换成||,空格换成/**/或%09
  • 用hex编码:把字符串转成十六进制,避免引号被过滤

我遇到过最麻烦的一种过滤是把空格全部删掉,导致union select变成unionselect。解决办法是用括号或注释代替空格:union(select)或union/**/select。

6.3 数据提取时的编码问题

提取中文数据时,经常会遇到乱码。原因是数据库的字符集和页面显示的字符集不一致。解决办法是用hex()函数把数据转成十六进制,拿到后再转回字符串:

?id=1' and extractvalue(1,concat(0x7e,hex((select 字段名 from 表名 limit 0,1)),0x7e))--+

拿到十六进制字符串后,用在线工具或Python的bytes.fromhex()转回可读文本。这个方法虽然多了一步,但能避免编码问题导致的数据丢失。

6.4 盲注脚本的编写要点

手工盲注效率太低,实际测试中一般会写脚本自动化。脚本的核心逻辑:

  1. 先猜长度:length(database())>n
  2. 再逐位猜字符:ascii(substring(database(),i,1))>mid
  3. 用二分法缩小范围
  4. 把结果拼接起来

写脚本时要注意:每次请求之间加一点延时,避免触发目标服务器的频率限制;对异常响应做重试;把已经猜出的部分缓存下来,避免重复请求。

我用Python写过一版,核心就是requests库发请求,根据响应内容或响应时间判断真假。脚本本身不复杂,关键是二分法的边界条件要处理好,否则容易死循环。

7. 从注入练习中真正该带走的东西

做完这一轮SQL注入的专项练习,我最大的感受是:工具能帮你快速拿到结果,但只有手工走一遍完整流程,才能真正理解每个环节为什么这么做。判断注入类型是为了确定payload的构造方式,确定字段数是为了让union查询能正常执行,找显示位是为了建立数据回显通道,报错注入和盲注则是在没有回显时的替代方案。每一步都有明确的意图,不是机械地套公式。

另一个体会是:防御SQL注入其实比攻击简单得多。参数化查询(预编译语句)能从根源上杜绝注入,因为SQL语句的结构在编译时就确定了,用户输入永远只是数据,不会被当作语法解析。我在实际开发中坚持一条原则:任何拼接SQL字符串的地方,都要改成参数化查询。如果实在做不到,至少要对输入做严格的类型校验和白名单过滤。

最后分享一个小技巧:练习注入时,养成记录每一步payload和对应响应的习惯。我一般会用一个表格记录“步骤、payload、响应特征、结论”,这样卡住的时候回头翻记录,往往能发现之前忽略的细节。这个习惯在排查其他类型漏洞时同样适用。

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

世界地图GeoJSON资源:开箱即用的国家边界数据与Web可视化实践

简介:这份资源整理了世界地图与主要国家的 GeoJSON 数据,面向从事数据可视化、Web 地图开发的前端工程师与 GIS 初学者,解决绘制世界或单国地图时缺少现成边界数据的问题。压缩包共 25 个文件,以 23 个 json 为主,涵盖…

作者头像 李华
网站建设 2026/10/16 10:47:23

深入解析编译器内建函数:优化技巧与避坑指南

1. 内建函数到底是什么 它和普通函数差在哪 1.1 从一次误用说起 先讲个真实经历。某次我给一个网络服务做性能优化,热点路径里有个数据包分类函数,里面有一段类似 if (is_special_packet(p)) 的判断。这个分支预测失败率很高,我当时的想法…

作者头像 李华
网站建设 2026/10/16 10:45:15

NSGA-III Matlab代码详解:从原理到实战,高维多目标优化一步到位

简介:该资源提供可在 MATLAB 平台直接运行的 NSGA-III 多目标进化算法完整实现,面向正在学习或应用多目标优化的科研人员、工程师及高年级本科生。代码遵循 NSGA-III 原始文献思路设计,涵盖参考点生成、初始种群构建、模拟二进制交叉&#xf…

作者头像 李华
网站建设 2026/10/16 10:43:54

HBase Shell 常用命令实战:从预分区到快照的运维指南

最近整理笔记,翻出我维护 HBase 集群这几年用过的高频 Shell 命令,发现真正日常就是那么二三十条,但每条背后都有值得说清楚的细节。比如很多人建表不预分区,写请求全压在一台 RegionServer 上;再比如 drop 之前忘了 d…

作者头像 李华
网站建设 2026/10/16 10:42:38

LabVIEW波峰波谷检测实战:从Peak Detector到数组遍历及参数调优

最近在调一套气动检测平台的采集程序,甲方要的指标很直接:每次气缸动作,上位机必须自动把压力波形的冲击峰值时刻标出来,漏一个峰都不行。这种需求在LabVIEW的日常项目里太常见了——振动监测要看极值区间,疲劳测试要统…

作者头像 李华