SQL Inject中文叫做SQL隱碼攻擊,是發生在web端的安全漏洞,主要是實現非法操作,例如欺騙伺服器執行非法查詢,他的危害在於駭客會有惡意獲取,甚至篡改資料庫資訊,繞過登入驗證,原理是針對程式設計師編寫資料庫程式的疏忽,通過執行SQL語句,實現目的性攻擊,他的流程可以分為判斷資料庫型別、判斷資料庫版本、判斷注入點、判斷注入型別、判斷資料欄位數、判斷顯示位、獲取資料庫中的資訊。
簡單來說就是通過web表單把SQL命令提交到資料庫,由於管理員沒有細緻的過濾使用者輸入的資料,造成字串拼接,進而惡意的SQL語句被執行,造成資料庫資訊洩露、網頁篡改、資料庫被惡意操作等後果。
從注入引數型別分類:數位型注入、字元型注入、搜尋型注入
從注入方法分:報錯注入、布林盲注、時間盲注、聯合查詢注入、堆疊注入、內聯查詢注入、寬位元組注入
從提交方式分:GET注入、POST注入、COOKIE注入、HTTP頭注入
尋找可能的漏洞站點,也就是目標站點。
通過站點的字尾名來判斷網站的使用的是哪一種資料庫,簡單的判斷可以觀察指令碼字尾,如果是「.asp」為字尾,則資料庫可能是access,如果是「.aspx」為字尾可能是MsSql,如果是「.php」為字尾的可能是mysql資料庫,如果是「.jsp」,可能是orcale資料庫。
是尋找站點存在的注入點,可以再URL中進行嘗試輸入引數後在拼接上引號,通過回顯可以判斷該站點傳輸方式,為GET或POST方式POST需要檢視表單資料,POST注入在表單中提交引號,而cookie注入可以通過burpsuite工具來判斷注入點。
判斷注入點的型別,如果加減法運算按照是否是數位型注入,如果單引號和頁面報錯資訊來進一步判斷是哪一種的字元型注入。
閉合我們輸入的SQL語句,通過註釋的方式來獲取資料,有以下幾種情況:
①當頁面有回顯但是沒有顯示位的時候,可以選擇報錯注入,常見的報錯注入利用函數有那麼幾個:floor()、exp()、updatexml()、exteractvalue()等函數。
②當頁面沒有明確的回顯資訊的時候,但是輸入正確和輸入錯誤的時候頁面不相同的情況下,可以考慮報錯注入,報錯注入常見的函數有:ascii()、substr()、length()、concat()等函數。
③當頁面沒有會回顯也沒有報錯資訊的時候,可以使用時間盲注去獲取資料庫中的資料,時間盲注常見的函數有:sleep()。
其他:當然還有很多其他的注入方式,比如:寬位元組注入、base64注入、cookie注入、http頭部注入、二次注入、堆疊注入等。
(1)獲取企業內部、個人未授權的隱私資訊,或一些機密資料。
(2)頁面內容偽造篡改。
(3)資料庫、伺服器、網路(內網、區域網)受到攻擊,嚴重時可導致伺服器癱瘓,無法正常執行。
(1)對資料庫進行嚴格的監控。
(2)對使用者提交的資料進行嚴格的把關,多次篩選過濾。
(3)對使用者內資料內容進行加密。
(4)程式碼層面最佳防禦sql漏洞方案:採用sql語句預編譯和繫結變數,是防禦sql注入的最佳方式。
我們在進行查詢的時候可能會遇到union聯合查詢我們利用Navicat Premium 15連結我們的dvwa的MySQL資料庫,然後我們找到dvwa然後右鍵右鍵點選選擇「設計表」。接下來我們將「first_name」、「last_name」、「user」等全部將預設排序的「utf8_unicode_ci」修改為「utf8_general_ci」。修改完成然後儲存,完成修改。就可以正常使用union聯合查詢語句了。
程式碼分析:
<?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加密需要自己解密查詢。
程式碼分析:
<?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#」語句,得到最後的結果。
程式碼分析:
<?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 #」,即可得到答案。
程式碼分析:
<?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 注入。