DVWA靶場實戰(七)——SQL Injection

2023-01-14 06:01:49

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 注入。