web安全之攻擊

轉自 知乎https://www.zhihu.com/question/22953267php

做者:潘良虎
連接:https://www.zhihu.com/question/22953267/answer/80141632
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

多年前寫過一篇 「 Web安全之SQL注入攻擊技巧與防範」,今天回頭再看依然有價值,就偷個懶,直接貼過來了。

Web安全簡史

在Web1.0時代,人們更可能是關注服務器端動態腳本語言的安全問題,好比將一個可執行腳本(俗稱Webshell)經過腳本語言的漏洞上傳到服務器上,從而得到服務器權限。在Web發展初期,隨着動態腳本語言的發展和普及,以及早期工程師對安全問題認知不足致使不少」安全血案」的發生,至今仍然遺留下許多歷史問題,好比PHP語言至今仍然沒法從語言自己杜絕「文件包含漏洞」(參見這裏),只能依靠工程師良好的代碼規範和安全意識。html


伴隨着Web2.0、社交網絡、微博等一系列新型互聯網產品的興起,基於Web環境的互聯網應用愈來愈普遍,Web攻擊的手段也愈來愈多樣,Web安全史上的一個重要里程碑是大約1999年發現的SQL注入攻擊,以後的XSS,CSRF等攻擊手段愈發強大,Web攻擊的思路也從服務端轉向了客戶端,轉向了瀏覽器和用戶。mysql


在安全領域,通常用帽子的顏色來比喻黑客的善與惡,白帽子是指那些工做在反黑客領域的技術專家,這個羣體是」善」的的象徵;而黑帽子則是指那些利用黑客技術形成破壞甚至謀取私利形成犯罪的羣體,他們是」惡」的表明。web


「白帽子」和」黑帽子」是兩個徹底對立的羣體。對於黑帽子而言,他們只要找到系統的一個切入點就能夠達到入侵破壞的目的,而白帽子必須將本身系統全部可能被突破的地方都設防,以保證系統的安全運行。算法


這看起來好像是不公平的,可是安全世界裏的規則就是這樣,可能咱們的網站1000處都佈防的很好,考慮的很周到,可是隻要有一個地方疏忽了,攻擊者就會利用這個點進行突破,讓咱們另外的1000處努力白費。sql


常見攻擊方式

通常說來,在Web安全領域,常見的攻擊方式大概有如下幾種:
一、SQL注入攻擊
二、跨站腳本攻擊 - XSS
三、跨站僞造請求攻擊 - CSRF
四、文件上傳漏洞攻擊
五、分佈式拒絕服務攻擊 - DDOSshell



限於篇幅,本篇只討論SQL注入攻擊的技巧與防範。

SQL注入常見攻擊技巧

SQL注入攻擊是Web安全史上的一個重要里程碑,它從1999年首次進入人們的視線,至今已經有十幾年的歷史了,雖然咱們如今已經有了很全面的防範對策,可是它的威力仍然不容小覷,SQL注入攻擊至今仍然是Web安全領域中的一個重要組成部分。數據庫

以PHP+MySQL爲例,讓咱們以一個Web網站中最基本的用戶系統來作實例演示,看看SQL注入到底是怎麼發生的。api


一、建立一個名爲demo的數據庫:瀏覽器

CREATE DATABASE `demo` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 

二、建立一個名爲user的數據表,並插入1條演示數據:

CREATE TABLE `demo`.`user` ( `uid` INT( 11 ) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '用戶uid', `username` VARCHAR( 20 ) NOT NULL COMMENT '用戶名', `password` VARCHAR( 32 ) NOT NULL COMMENT '用戶密碼' ) ENGINE = INNODB; INSERT INTO `demo`.`user` (`uid`, `username`, `password`) VALUES ('1', 'plhwin', MD5('123456')); 

實例一

經過傳入username參數,在頁面打印出這個會員的詳細信息,編寫 userinfo.php 程序代碼:

<?php header('Content-type:text/html; charset=UTF-8'); $username = isset($_GET['username']) ? $_GET['username'] : ''; $userinfo = array(); if($username){ //使用mysqli驅動鏈接demo數據庫 $mysqli = new mysqli("localhost", "root", "root", 'demo'); $sql = "SELECT uid,username FROM user WHERE username='{$username}'"; //mysqli multi_query 支持執行多條MySQL語句 $query = $mysqli->multi_query($sql); if($query){ do { $result = $mysqli->store_result(); while($row = $result->fetch_assoc()){ $userinfo[] = $row; } if(!$mysqli->more_results()){ break; } } while ($mysqli->next_result()); } } echo '<pre>',print_r($userinfo, 1),'</pre>'; 

上面這個程序要實現的功能是根據瀏覽器傳入的用戶名參數,在頁面上打印出這個用戶的詳細信息,程序寫的這麼複雜是由於我採用了mysqli的驅動,以便能使用到 multi_query 方法來支持同時執行多條SQL語句,這樣能更好的說明SQL注入攻擊的危害性。

假設咱們能夠經過 http://localhost/test/userinfo.php?username=plhwin 這個URL來訪問到具體某個會員的詳情,正常狀況下,若是瀏覽器裏傳入的username是合法的,那麼SQL語句會執行:

SELECT uid,username FROM user WHERE username='plhwin'

可是,若是用戶在瀏覽器裏把傳入的username參數變爲 plhwin';SHOW TABLES-- hack,也就是當URL變爲 http://localhost/test/userinfo.php?username=plhwin';SHOW TABLES-- hack 的時候,此時咱們程序實際執行的SQL語句變成了:

SELECT uid,username FROM user WHERE username='plhwin';SHOW TABLES-- hack' 

注意:在MySQL中,最後連續的兩個減號表示忽略此SQL減號後面的語句,我本機的MySQL版本號爲5.6.12,目前幾乎全部SQL注入實例都是直接採用兩個減號結尾,可是實際測試,這個版本號的MySQL要求兩個減號後面必需要有空格才能正常注入,而瀏覽器是會自動刪除掉URL尾部空格的,因此咱們的注入會在兩個減號後面統一添加任意一個字符或單詞,本篇文章的SQL注入實例統一以 -- hack 結尾。


通過上面的SQL注入後,本來想要執行查詢會員詳情的SQL語句,此時還額外執行了 SHOW TABLES; 語句,這顯然不是開發者的本意,此時能夠在瀏覽器裏看到頁面的輸出:

Array (  [0] => Array  (  [uid] => 1  [username] => plhwin  )  [1] => Array  (  [Tables_in_demo] => user  ) ) 

你能清晰的看到,除了會員的信息,數據庫表的名字user也被打印在了頁面上,若是做惡的黑客此時將參數換成 plhwin';DROP TABLE user-- hack,那將產生災難性的嚴重結果,當你在瀏覽器中執行 http://localhost/test/userinfo.php?username=plhwin';DROP TABLE user-- hack 這個URL後,你會發現整個 user 數據表都消失不見了。


經過上面的例子,你們已經認識到SQL注入攻擊的危害性,可是仍然會有人心存疑問,MySQL默認驅動的mysql_query方法如今已經不支持多條語句同時執行了,大部分開發者怎麼可能像上面的演示程序那樣又麻煩又不安全。


是的,在PHP程序中,MySQL是不容許在一個mysql_query中使用分號執行多SQL語句的,這使得不少開發者都認爲MySQL自己就不容許多語句執行了,但實際上MySQL早在4.1版本就容許多語句執行,經過PHP的源代碼,咱們發現其實只是PHP語言自身限制了這種用法,具體狀況你們能夠看看這篇文章「PHP+MySQL多語句執行」。


實例二

若是系統不容許同時執行多條SQL語句,那麼SQL注入攻擊是否是就再也不這麼可怕呢?答案是否認的,咱們仍然以上面的user數據表,用Web網站中經常使用的會員登陸系統來作另一個場景實例,編寫程序login.php,代碼以下:

<?php if($_POST){ $link = mysql_connect("localhost", "root", "root"); mysql_select_db('demo', $link); $username = empty($_POST['username']) ? '' : $_POST['username']; $password = empty($_POST['password']) ? '' : $_POST['password']; $md5password = md5($password); $sql = "SELECT uid,username FROM user WHERE username='{$username}' AND password='{$md5password}'"; $query = mysql_query($sql, $link); $userinfo = mysql_fetch_array($query, MYSQL_ASSOC); if(!empty($userinfo)){ //登陸成功,打印出會員信息 echo '<pre>',print_r($userinfo, 1),'</pre>'; } else { echo "用戶名不存在或密碼錯誤!"; } } ?> <!DOCTYPE html> <html> <head>  <meta charset="utf-8">  <title>Web登陸系統SQL注入實例</title> </head> <body>  <form name="LOGIN_FORM" method="post" action="">  登陸賬號: <input type="text" name="username" value="" size=30 /><br /><br />  登陸密碼: <input type="text" name="password" value="" size=30 /><br /><br />  <input type="submit" value="登陸" />  </form> </body> </html> 

此時若是輸入正確的用戶名 plhwin 和密碼 123456,執行的SQL語句爲:

SELECT uid,username FROM user WHERE username='plhwin' AND password='e10adc3949ba59abbe56e057f20f883e'

上面語句沒有任何問題,能夠看到頁面打印出了登陸成功後的會員信息,但若是有搗蛋鬼輸入的用戶名爲 plhwin' AND 1=1-- hack,密碼隨意輸入,好比 aaaaaa,那麼拼接以後的SQL查詢語句就變成了以下內容:

SELECT uid,username FROM user WHERE username='plhwin' AND 1=1-- hack' AND password='0b4e7a0e5fe84ad35fb5f95b9ceeac79'

執行上面的SQL語句,由於 1=1 是永遠成立的條件,這意味着黑客只須要知作別人的會員名,無需知道密碼就能順利登陸到系統。


如何肯定SQL注入漏洞

經過以上的實例,咱們仍然還會有疑問:黑客並不知道咱們程序代碼的邏輯和SQL語句的寫法,他是如何肯定一個網站是否存在SQL注入漏洞呢?通常說來有如下2種途徑:



一、錯誤提示

若是目標Web網站開啓了錯誤顯示,攻擊者就能夠經過反覆調整發送的參數、查看頁面打印的錯誤信息,推測出Web網站使用的數據庫和開發語言等重要信息。



二、盲注

除非運維人員疏忽,不然大部分的Web運營網站應該都關閉了錯誤提示信息,此時攻擊者通常會採用盲注的技巧來進行反覆的嘗試判斷。 仍然以上面的數據表user爲例,咱們以前的查看會員詳情頁面的url地址爲 userinfo.php?username=plhwin,此時黑客分別訪問 userinfo.php?username=plhwin' AND 1=1-- hack userinfo.php?username=plhwin' AND 1=2-- hack,若是前者訪問能返回正常的信息然後者不能,就基本能夠判斷此網站存在SQL注入漏洞,由於後者的 1=2 這個表達式永遠不成立,因此即便username傳入了正確的參數也沒法經過,由此能夠推斷這個頁面存在SQL注入漏洞,而且能夠經過username參數進行注入。



如何防護SQL注入

對於服務器配置層面的防範,應該保證生產環境的Webserver是關閉錯誤信息的,好比PHP在生產環境的配置文件php.ini中的display_errors應該設置爲Off,這樣就關閉了錯誤提示,下面咱們更多的從編碼的角度來看看如何防範SQL注入。


上面用兩個實例分析了SQL注入攻擊的技巧,能夠看到,但凡是有SQL注入漏洞的程序,都是由於程序要接受來自客戶端用戶輸入的變量或URL傳遞的參數,而且這個變量或參數是組成SQL語句的一部分,對於用戶輸入的內容或傳遞的參數,咱們應該要時刻保持警戒,這是安全領域裏的「外部數據不可信任」的原則,縱觀Web安全領域的各類攻擊方式,大多數都是由於開發者違反了這個原則而致使的,因此天然能想到的,就是從變量的檢測、過濾、驗證下手,確保變量是開發者所預想的。


一、檢查變量數據類型和格式

若是你的SQL語句是相似where id={$id}這種形式,數據庫裏全部的id都是數字,那麼就應該在SQL被執行前,檢查確保變量id是int類型;若是是接受郵箱,那就應該檢查並嚴格確保變量必定是郵箱的格式,其餘的類型好比日期、時間等也是一個道理。總結起來:只要是有固定格式的變量,在SQL語句執行前,應該嚴格按照固定格式去檢查,確保變量是咱們預想的格式,這樣很大程度上能夠避免SQL注入攻擊。


好比,咱們前面接受username參數例子中,咱們的產品設計應該是在用戶註冊的一開始,就有一個用戶名的規則,好比5-20個字符,只能由大小寫字母、數字以及一些安全的符號組成,不包含特殊字符。此時咱們應該有一個check_username的函數來進行統一的檢查。不過,仍然有不少例外狀況並不能應用到這一準則,好比文章發佈系統,評論系統等必需要容許用戶提交任意字符串的場景,這就須要採用過濾等其餘方案了。


二、過濾特殊符號

對於沒法肯定固定格式的變量,必定要進行特殊符號過濾或轉義處理。以PHP爲例,一般是採用addslashes函數,它會在指定的預約義字符前添加反斜槓轉義,這些預約義的字符是:單引號 (') 雙引號 (") 反斜槓 (\) NULL。


來看2條SQL語句:

$uid = isset($_GET['uid']) ? $_GET['uid'] : 0; $uid = addslashes(uid); $sql = "SELECT uid,username FROM user WHERE uid='{$uid}'"; 

以及

$uid = isset($_GET['uid']) ? $_GET['uid'] : 0; $uid = addslashes(uid); $sql = "SELECT uid,username FROM user WHERE uid={$uid}"; 

上面兩個查詢語句都通過了php的addslashes函數過濾轉義,但在安全性上卻大不相同,在MySQL中,對於int類型字段的條件查詢,上面個語句的查詢效果徹底同樣,因爲第一句SQL的變量被單引號包含起來,SQL注入的時候,黑客面臨的首要問題是必需要先閉合前面的單引號,這樣才能使後面的語句做爲SQL執行,而且還要註釋掉原SQL語句中的後面的單引號,這樣才能夠成功注入,因爲代碼裏使用了addslashes函數,黑客的攻擊會無從下手,但第二句沒有用引號包含變量,那黑客也不用考慮去閉合、註釋,因此即使一樣採用addslashes轉義,也仍是存在SQL攻擊漏洞。


對於PHP程序+MySQL構架的程序,在動態的SQL語句中,使用單引號把變量包含起來配合addslashes函數是應對SQL注入攻擊的有效手段,但這作的還不夠,像上面的2條SQL語句,根據「檢查數據類型」的原則,uid都應該通過intval函數格式爲int型,這樣不只能有效避免第二條語句的SQL注入漏洞,還能使得程序看起來更天然,尤爲是在NoSQL(如MongoDB)中,變量類型必定要與字段類型相匹配才能夠。


從上面能夠看出,第二個SQL語句是有漏洞的,不過因爲使用了addslashes函數,你會發現黑客的攻擊語句也存在不能使用特殊符號的條件限制,相似where username='plhwin'這樣的攻擊語句是無法執行的,可是黑客能夠將字符串轉爲16進制編碼數據或使用char函數進行轉化,一樣能達到相同的目的,若是對這部份內容感興趣,能夠點擊這裏查看。並且因爲SQL保留關鍵字,如「HAVING」、「ORDER BY」的存在,即便是基於黑白名單的過濾方法仍然會有或多或少問題,那麼是否還有其餘方法來防護SQL注入呢?


三、綁定變量,使用預編譯語句

MySQL的mysqli驅動提供了預編譯語句的支持,不一樣的程序語言,都分別有使用預編譯語句的方法,咱們這裏仍然以PHP爲例,編寫userinfo2.php代碼:

<?php header('Content-type:text/html; charset=UTF-8'); $username = isset($_GET['username']) ? $_GET['username'] : ''; $userinfo = array(); if($username){ //使用mysqli驅動鏈接demo數據庫 $mysqli = new mysqli("localhost", "root", "root", 'demo'); //使用問號替代變量位置 $sql = "SELECT uid,username FROM user WHERE username=?"; $stmt = $mysqli->prepare($sql); //綁定變量 $stmt->bind_param("s", $username); $stmt->execute(); $stmt->bind_result($uid, $username); while ($stmt->fetch()) { $row = array(); $row['uid'] = $uid; $row['username'] = $username; $userinfo[] = $row; } } echo '<pre>',print_r($userinfo, 1),'</pre>'; 

從上面的代碼能夠看到,咱們程序裏並無使用addslashes函數,可是瀏覽器裏運行 http://localhost/test/userinfo2.php?username=plhwin' AND 1=1-- hack 裏得不到任何結果,說明SQL漏洞在這個程序裏並不存在。


實際上,綁定變量使用預編譯語句是預防SQL注入的最佳方式,使用預編譯的SQL語句語義不會發生改變,在SQL語句中,變量用問號?表示,黑客即便本事再大,也沒法改變SQL語句的結構,像上面例子中,username變量傳遞的 plhwin' AND 1=1-- hack 參數,也只會看成username字符串來解釋查詢,從根本上杜絕了SQL注入攻擊的發生。


數據庫信息加密安全

相信你們都還對2011年爆出的CSDN拖庫事件記憶猶新,這件事情致使CSDN處在風口浪尖被你們痛罵的緣由就在於他們居然明文存儲用戶的密碼,這引起了科技界對用戶信息安全尤爲是密碼安全的強烈關注,咱們在防範SQL注入的發生的同時,也應該未雨綢繆,說不定下一個被拖庫的就是你,誰知道呢。


在Web開發中,傳統的加解密大體能夠分爲三種:

一、對稱加密:

即加密方和解密方都使用相同的加密算法和密鑰,這種方案的密鑰的保存很是關鍵,由於算法是公開的,而密鑰是保密的,一旦密匙泄露,黑客仍然能夠輕易解密。常見的對稱加密算法有:AES、DES等。


二、非對稱加密:

即便用不一樣的密鑰來進行加解密,密鑰被分爲公鑰和私鑰,用私鑰加密的數據必須使用公鑰來解密,一樣用公鑰加密的數據必須用對應的私鑰來解密,常見的非對稱加密算法有:RSA等。


三、不可逆加密:

利用哈希算法使數據加密以後沒法解密回原數據,這樣的哈希算法經常使用的有:md五、SHA-1等。


在咱們上面登陸系統的示例代碼中,$md5password = md5($password); 從這句代碼能夠看到採用了md5的不可逆加密算法來存儲密碼,這也是多年來業界經常使用的密碼加密算法,可是這仍然不安全。爲何呢?


這是由於md5加密有一個特色:一樣的字符串通過md5哈希計算以後生成的加密字符串也是相同的,因爲業界採用這種加密的方式由來已久,黑客們也準備了本身強大的md5彩虹表來逆向匹配加密前的字符串,這種用於逆向反推MD5加密的彩虹表在互聯網上隨處可見,在Google裏使用md5 解密做爲關鍵詞搜索,一下就能找到md5在線破解網站,把咱們插入用戶數據時候的MD5加密字符串e10adc3949ba59abbe56e057f20f883e填入進去,瞬間就能獲得加密前的密碼:123456。固然也並非每個都能成功,但能夠確定的是,這個彩虹表會愈來愈完善。


因此,咱們有迫切的需求採用更好的方法對密碼數據進行不可逆加密,一般的作法是爲每一個用戶肯定不一樣的密碼加鹽(salt)後,再混合用戶的真實密碼進行md5加密,如如下代碼:

<?php //用戶註冊時候設置的password $password = $_POST['password']; //md5加密,傳統作法直接將加密後的字符串存入數據庫,但這不夠,咱們繼續改良 $passwordmd5 = md5($password); //爲用戶生成不一樣的密碼鹽,算法能夠根據本身業務的須要而不一樣 $salt = substr(uniqid(rand()), -6); //新的加密字符串包含了密碼鹽 $passwordmd5 = md5($passwordmd5.$salt); 

小結

一、不要隨意開啓生產環境中Webserver的錯誤顯示。二、永遠不要信任來自用戶端的變量輸入,有固定格式的變量必定要嚴格檢查對應的格式,沒有固定格式的變量須要對引號等特殊字符進行必要的過濾轉義。三、使用預編譯綁定變量的SQL語句。四、作好數據庫賬號權限管理。五、嚴格加密處理用戶的機密信息。

相關文章
相關標籤/搜索