DVWA靶场实战(七)——SQL Injection

DVWA靶场实战(七)

七、SQL Injection:

1.漏洞原理:

  SQL Inject中文叫做SQL注入,是发生在web端的安全漏洞,主要是实现非法操作,例如欺骗服务器执行非法查询,他的危害在于黑客会有恶意获取,甚至篡改数据库信息,绕过登录验证,原理是针对程序员编写数据库程序的疏忽,通过执行SQL语句,实现目的性攻击,他的流程可以分为判断数据库类型、判断数据库版本、判断注入点、判断注入类型、判断数据字段数、判断显示位、获取数据库中的信息。

  简单来说就是通过web表单把SQL命令提交到数据库,由于管理员没有细致的过滤用户输入的数据,造成字符串拼接,进而恶意的SQL语句被执行,造成数据库信息泄露、网页篡改、数据库被恶意操作等后果。

2.SQL Injection分类:

  从注入参数类型分类:数字型注入、字符型注入、搜索型注入

  从注入方法分:报错注入、布尔盲注、时间盲注、联合查询注入、堆叠注入、内联查询注入、宽字节注入

  从提交方式分:GET注入、POST注入、COOKIE注入、HTTP头注入

3.SQL注入漏洞的利用思路:

(1)第一步:

  寻找可能的漏洞站点,也就是目标站点。

(2)第二步:

  通过站点的后缀名来判断网站的使用的是哪一种数据库,简单的判断可以观察脚本后缀,如果是“.asp”为后缀,则数据库可能是access,如果是“.aspx”为后缀可能是MsSql,如果是“.php”为后缀的可能是mysql数据库,如果是“.jsp”,可能是orcale数据库。

(3)第三步:

  是寻找站点存在的注入点,可以再URL中进行尝试输入参数后在拼接上引号,通过回显可以判断该站点传输方式,为GET或POST方式POST需要查看表单数据,POST注入在表单中提交引号,而cookie注入可以通过burpsuite工具来判断注入点。

(4)第四步:

  判断注入点的类型,如果加减法运算按照是否是数字型注入,如果单引号和页面报错信息来进一步判断是哪一种的字符型注入。

(5)第五步:

  闭合我们输入的SQL语句,通过注释的方式来获取数据,有以下几种情况:

    ①当页面有回显但是没有显示位的时候,可以选择报错注入,常见的报错注入利用函数有那么几个:floor()、exp()、updatexml()、exteractvalue()等函数。

    ②当页面没有明确的回显信息的时候,但是输入正确和输入错误的时候页面不相同的情况下,可以考虑报错注入,报错注入常见的函数有:ascii()、substr()、length()、concat()等函数。

    ③当页面没有会回显也没有报错信息的时候,可以使用时间盲注去获取数据库中的数据,时间盲注常见的函数有:sleep()。

    其他:当然还有很多其他的注入方式,比如:宽字节注入、base64注入、cookie注入、http头部注入、二次注入、堆叠注入等。

4.漏洞危害:

  (1)获取企业内部、个人未授权的隐私信息,或一些机密数据。

  (2)页面内容伪造篡改。

  (3)数据库、服务器、网络(内网、局域网)受到攻击,严重时可导致服务器瘫痪,无法正常运行。

5.防御以及修复措施:

  (1)对数据库进行严格的监控。

  (2)对用户提交的数据进行严格的把关,多次筛选过滤。

  (3)对用户内数据内容进行加密。

  (4)代码层面最佳防御sql漏洞方案:采用sql语句预编译和绑定变量,是防御sql注入的最佳方式。

6.注意事项:

  我们在进行查询的时候可能会遇到union联合查询我们利用Navicat Premium 15链接我们的dvwa的MySQL数据库,然后我们找到dvwa然后右键右键点击选择“设计表”。接下来我们将“first_name”、“last_name”、“user”等全部将默认排序的“utf8_unicode_ci”修改为“utf8_general_ci”。修改完成然后保存,完成修改。就可以正常使用union联合查询语句了。

7.实战:

(1)Low:

  代码分析:

<?php

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

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            // 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"]);
            break;
        case SQLITE:
            global $sqlite_db_connection;

            #$sqlite_db_connection = new SQLite3($_DVWA['SQLITE_DB']);
            #$sqlite_db_connection->enableExceptions(true);

            $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
                    $html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
                }
            } else {
                echo "Error in fetch ".$sqlite_db->lastErrorMsg();
            }
            break;
    } 
}

?> 

  可以看出来基本上没有防护,所以我们直接开始攻击了。

  首先利用语句“1’and’1’=’1”和“1’and’1’=’2”来判断是否有注入点。以下为两种语句的不同回显,可以判断出是有注入点的。

  判断完注入点我们尝试获取他的判断他的列数,利用“1’order by 1#”语句替换“1”来判断行数,这里我们“#”或者“--+”的作用是注释掉原本语句后面的内容,让我们能够自如的进行查询。然后我们最后查询到“1’order by 3#”就弹出报错,就说明列数应该为两列。

 

  判断完列数后,我们开始利用联合查询语句,这里使用“-1’union select 1,2#”,来判断显示位。我们这里可以看见回显了另外一个语句,就说明显示位为2,一般靶场中比如sqli-labs那种就会需要将前面的“1”改为“-1”,也就是“-1’union select 1,2#”才会正常回显我们查询的内容。这里我们也可以,之后的演示排除干扰就将第一行结果屏蔽了。

 

  这里我们收集数据库名称利用database()函数,它的作用是返回数据库名,我们在后续查询中是需要数据库才能进行的所以利用语句“-1’union select 1,database()#”接下来进行查询,得到结果数据库名称为“dvwa”。

  我们收集完以上的数据库信息后,我们再查询一下数据库的版本,因为mysql数据库注入大概是以5.0版本为分界线,5.0以下没有information表,5.0以上则都有information表。这里的MySQL版本为5.7.26,那么我们就可以查询information表来找到数据库的表名。

 

  接下来,我们查询dvwa数据库有哪些表名,利用联合查询语句“-1'union select 1,table_name from information_schema.tables where table_schema='dvwa'#”来进行对表名的查询。得到两个表名“guestbook”和“users”。

 

  我们查询好表名后,我们查询列名,利用语句“-1'union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users')#”得到“user_id,first_name,last_name,user,password,avatar,last_login,failed_login

”的结果,这个时候我们就可以知道我们应该查询“user”和“password”。

  然后我们查询好表名为“users”和列名“user”、列名“password”后,我们利用查询语句“-1’union select user,password from users”,然后查询得到账号和密码,密码是MD5加密需要自己解密查询。

 

(2)Medium:

  代码分析:

<?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
                $html .= "<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
                    $html .= "<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"]);
?>

  使用POST提交方式,还使用了转义预防SQL注入。

  我们开始攻击,首先我们打开BP进行截包,得到如下的数据包。并将其发送到“Repeater”,方便接下来的注入回显操作。

 

  我们同样利用“1 and 1=1”和“1 and 1=2”来判断,是否这里为注入点,观察两者的有差异且页面未报错,就说明这里是注入点。

 

  这里我们可以看到,同样是在“3”的时候,我们这里用“order by 3”查询返回报错,所以我们这里判断前面只查询两列。利用语句“1 union select 1,2#”来进行验证,返回正确,验证成功。

 

  此时同样判断完列数后,我们同时对版本和数据库名称进行查询,利用语句“1 union select database(),version()#”。此时观察回显,发现同时回显了数据库名称和数据库版本dvwa和5.7.26。

  在查询完数据库后,我们先查询表名,然后查询列名,利用语句“1 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=’dvwa’)#”,但发现报错,没有任何反应,将“dvwa”改为“database()”后查询得到“users”表名后。后来发现是对单引号进行了转义。所以我们将单引号连同users一起写为16进制表示为“0x75736572”。

  查询列名“1 union select (select group_concat(column_name) from information_schema.columns where table_name=0x75736572),(select group_concat(table_name) from information_schema.tables where table_schema=database())”,得到关键列名“user”和“password”。

 

  最后我们利用,“1 union select user,password from users#”语句,得到最后的结果。

 

(3)High:

  代码分析:

<?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
                $html .= "<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
                    $html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
                }
            } else {
                echo "Error in fetch ".$sqlite_db->lastErrorMsg();
            }
            break;
    }
}

?> 

  使用了session 获取id 值,闭合方式单引号闭合。

  虽然用了session来获取id值,但是同样也是数字型注入,利用同样的方法,然后最后用“-1’union select user,password from users #”,即可得到答案。

 

(4)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 )) {
        $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
                    $html .= "<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
                        $html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
                    }
                }

                break;
        }
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

  此为防御模板,CSRF、检测 id 是否是数字,prepare 预编译语句的优势在于归纳为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止 SQL 注入。

posted @ 2023-01-13 22:52  wybsignal  阅读(384)  评论(0编辑  收藏  举报