DGZ's Blog.

sql注入入门记录(未完)

Word count: 2.7kReading time: 11 min
2020/04/13 Share

由于比较泛,所以就单独拿一篇文章来记录,仅是入门
参考赵师傅b站:https://www.bilibili.com/video/BV1XE411f7C8
pockr教程

利用wampserver的集成套件,使用里面的mysql数据库
cmd中输入mysql -u root -p 回车后再输入密码
基础命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
show databases;   
create database test;
user test;
create table users(id int(11),name varchar(255));
show tables;
desc users;
insert into users values(1,'dgz'),(2,'admin'),(4,'test');
select * from users;
select * from users order by id;
select * from users order by id\G; 的作用是将查到的结构旋转90度变成纵向
select * from users order by id desc; #根据id大小倒序排
select * from users limit 1 offset 0; #偏移0选一行值
select * from users limit 2 offset 1; #偏移1选两行值,即选第二、三行数据
update users set name='test2' where id = 1;
update users set name='test2' where id = 1\G; \G的作用是将查到的结构旋转90度变成纵向
delete from users where id=2;

由于本地mysql对于字符型与整数型,会存在一个自动转换的过程,所以对于常见sql注入复现的题目本地测试会有点差别。
GjASeA.png
参考:https://blog.csdn.net/yjgithub/article/details/74534066
原本是VARCHAR类型,不加引号,在搜索时需要加多一个转换操作,将INT转换VARCHAR,会多花一点时间,但是对于我这么小的表几乎没有影响,搜索的结果都是一样的。

DVWA靶场sql injection

Low级别

查看源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php 

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

// Check database
$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"];

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

mysqli_close($GLOBALS["___mysqli_ston"]);
}

?>

id是字符型的,所以构造sql语句时需要单引号闭合,而且看得出没有过滤,没有白名单黑名单

当然也可以测试一下,输入1' and 1=1#1' and 1=2#

对于此处这个sql语句,代入之后是SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1#';#号的作用是注释掉后面的引号,而没有注释掉分号,

但是本地测试可以看到#对分号有影响,注释掉分号了,因为分号没了就不是完整的sql语句了所以无法运行搜索,所以是有区别的。

Gjz7b6.png

输入1' and 1=2#没有回显
Gjz2UU.png
对于1' or 1=1# 某一些服务只会回显id=1,而不会所有都显示出来,所以最好可以改成0' or 1=1#,让前面的查询无效,在dvwa下此时是所有信息都会显示出来。

GjzfC4.png
对于order by查字段数,原理其实是order by是一个排序的功能符?(我可以这么说吗)

GjzR5F.png

如图的测试,相同id以age大小排序

而在sql注入中测试时
GjzgET.png

order by实际上是以字段序数和字段名称进行排序,如图order by 3order by age结果是一样的,而order by 4报错是因为没有第四列,所以通过order by可以查字段数,常用的方法是二分法查询。
Gjzh8J.png

所以在dvwa中,这样的语句可以有正确回显,而当order by 3时就会报错,因为"SELECT first_name, last_name FROM users WHERE user_id = '$id';";只有两个字段,超过2就会出错

对于union select联合查询,union运算符可以将两个或者两个以上select语句的查询结果集合合并成一个结果集显示,即执行联合查询。需要注意在使用union查询的时候需要和主查询的列数相同,其需要前后搜索的字段个数相同,也就是一般用order by知道字段数之后才union select

Gjz429.png

如图,前后select的字段必须相同,不然无法联合查询,排序1,2,3是要知道字段对应的位置,大概是因为原表中没有id=1,name=2,age=3的数据才这么回显出来。

当我们输入1' union select version(),@@version_compile_os#

Gjz5vR.png

在第二个结果返回了当前数据库的版本和当前操作系统。

之后就是通过union select获取到我们想要的关键信息

其中,information_schema是mysql自带的一个库,可以叫元数据库,这个库保存了mysql所有数据库的信息,如数据库名、数据库的表与访问权限等等,该数据库拥有一个名为TABLES的数据表,该表包含两个字段table_name和table_schema,分别记录DBMS中的存储的表名和表名所在的数据库。

更详细介绍可见:https://www.cnblogs.com/lcbwwy/p/12670465.html

GjzoK1.png

所以接下来的关键信息获取,要通过元数据库获取

利用database()获得数据库名称

GjzTDx.png

然后1' union select 1,table_name from information_schema.TABLES where table_schema='dvwa'#,得到库里面的表名users和guestbook,然后猜想users实际一点

GjzbVK.png

接下来1' union select 1,column_name from information_schema.COLUMNS where table_name='users'#

对大小写不敏感,其实全部表名库名小写大写都行

GjzL5D.png

1' union select user,password from users#

GjzXPe.png

得到了用户名和密码

Medium级别

变成了选择框

Gjzv2d.png

查看源代码

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

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

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

$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>";
}

}

// 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"]);
?>

看到id是整数型的,而且选择后url没有对应id显示,应该是post型传参

burp抓包,谷歌浏览器需要在设置里面的管理证书-受信任的根证书颁发机构-导入burp的证书

残废型选手上线,一直抓不到dvwa的包,后来一看

GjzxxA.png

都不代理了怎么还能抓到包?赶紧删掉然后就可以抓包了

源代码中$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);会转义字符串中的特殊字符:包括:NUL(ASCII 0)、\n、\r、\、’、” 和 Control-Z。

会把我们常用的单引号给转义了,前面会加一个反斜杠

然后我们这里可以利用16进制绕过引号的过滤

输入2 union select 1, group_concat(table_name) from information_schema.tables where table_schema=database()#

GvSSKI.png
对比上面low级别的,我们也可以看到group_concat的作用就是将输出结果用逗号连在一起,

2 union select 1, group_concat(column_name) from information_schema.columns where table_name=0x7573657273#

GvSprt.png
2 union select user,password from users#

GvS9qP.png

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

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

// 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);
}

?>

可以看到依然是没有过滤,只是通过LIMIT 1限制了返回一行值,但是我们依然可以通过#注释掉,所以没有什么用

因此直接用low级别的payload就可以了

需要特别提到的是,High级别的查询提交页面与查询结果显示页面不是同一个,也没有执行302跳转,这样做的目的是为了防止一般的sqlmap工具注入,因为sqlmap在注入过程中,无法在查询提交页面上获取查询的结果,没有了反馈,也就没办法进一步注入。

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
<?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 )) {
// 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>";
}
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

这种防御据称是不可以注入的,因为利用了PDO技术

PDO技术:PHP数据对象(PHP Data Object)。在生成网页时,许多PHP脚本通常都会执行除参数之外其他部分完全相同的查询语句,针对这种重复执行一个查询,每次迭代使用不同的参数情况,PDO提供了一种名为预处理语句(prepared statement)的机制。它可以将整个SQL命令向数据库服务器发送一次,以后只有参数发生变化,数据库服务器只需对命令的结构做一次分析就够了,即编译一次,可以多次执行。会在服务器上缓存查询的语句和执行过程,而只在服务器和客户端之间传输有变化的列值,以此来消除这些额外的开销。这不仅大大减少了需要传输的数据量,还提高了命令的处理效率。可以有效防止SQL注入,在执行单个查询时快于直接使用query()或exec()的方法,速度快且安全,推荐使用。

1
2
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); 
$data->bindParam( ':id', $id, PDO::PARAM_INT );

我的理解就是由于使用的是同一个sql语句进行查询,只有参数发生变化,利用PDO就可以编译一次,多次执行,不用重复编译,减少时间,此外划清了代码与数据的界限,有效防御SQL注入。

还有利用is_numeric($id)函数来判断输入的id是否是数字or数字字符串,满足条件才知晓query查询语句

1
2
3
4
if( $data->rowCount() == 1 ) { 
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

只有当返回的查询结果数量为一个记录时,才会成功输出,这样就有效预防了暴库

Anti-CSRF token机制的加入了进一步提高了安全性,session_token是随机生成的动态值,每次向服务器请求,客户端都会携带最新从服务端下发的session_token值向服务器请求作匹配验证,相互匹配才会验证通过

CATALOG
  1. 1. DVWA靶场sql injection
    1. 1.1. Low级别
    2. 1.2. Medium级别
    3. 1.3. High级别
    4. 1.4. Impossible级别