1. 程式人生 > >Web安全之SQL注入攻擊技巧與防範

Web安全之SQL注入攻擊技巧與防範

在Web1.0時代,人們更多是關注伺服器端動態指令碼語言的安全問題,比如將一個可執行指令碼(俗稱Webshell)通過指令碼語言的漏洞上傳到伺服器上,從而獲得伺服器許可權。在Web發展初期,隨著動態指令碼語言的發展和普及,以及早期工程師對安全問題認知不足導致很多”安全血案”的發生,至今仍然遺留下許多歷史問題,比如PHP語言至今仍然無法從語言本身杜絕「檔案包含漏洞」(參見這裡),只能依靠工程師良好的程式碼規範和安全意識。

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

在安全領域,一般用帽子的顏色來比喻黑客的善與惡,白帽子是指那些工作在反黑客領域的技術專家,這個群體是”善”的的象徵;而黑帽子則是指那些利用黑客技術造成破壞甚至謀取私利造成犯罪的群體,他們是”惡”的代表。

“白帽子”和”黑帽子”是兩個完全對立的群體。對於黑帽子而言,他們只要找到系統的一個切入點就可以達到入侵破壞的目的,而白帽子必須將自己系統所有可能被突破的地方都設防,以保證系統的安全執行。

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

常見攻擊方式
一般說來,在Web安全領域,常見的攻擊方式大概有以下幾種:
1、SQL注入攻擊
2、跨站指令碼攻擊 - XSS
3、跨站偽造請求攻擊 - CSRF
4、檔案上傳漏洞攻擊
5、分散式拒絕服務攻擊 - DDOS

說個題外話,本來這篇文章一開始的標題叫做 「Web安全之常見攻擊方法與防範」,我原本想把上面的這5種方法都全部寫在一篇文章裡,可是剛寫完第一個SQL注入攻擊的時候,就發現文章篇幅已經不短了,又很難再進行大幅度的精簡,所以索性把Web安全分成一個系列,分多篇文章來呈現給大家,下面你看到的就是第一篇「Web安全之SQL注入攻擊的技巧與防範」。

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

以PHP+MySQL為例,讓我們以一個Web網站中最基本的使用者系統來做例項演示,看看SQL注入究竟是怎麼發生的。

1、建立一個名為demo的資料庫:

CREATEDATABASE`demo`DEFAULTCHARACTERSET utf8 COLLATE utf8_general_ci;

2、建立一個名為user的資料表,並插入1條演示資料:

CREATETABLE`demo`.`user` (
`uid`INT( 11 ) NOTNULL AUTO_INCREMENT PRIMARY KEYCOMMENT'使用者uid',
`username`VARCHAR( 20 ) NOTNULLCOMMENT'使用者名稱',
`password`VARCHAR( 32 ) NOTNULLCOMMENT'使用者密碼'
) ENGINE = INNODB;
INSERTINTO`demo`.`user` (`uid`, `username`, `password`) VALUES ('1', 'plhwin', MD5('123456'));

例項一

通過傳入username引數,在頁面打印出這個會員的詳細資訊,編寫 userinfo.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'
',print_r($userinfo, 1),'
'</span>;</span>
</pre></span>
上面這個程式要實現的功能是根據瀏覽器傳入的使用者名稱引數,在頁面上打印出這個使用者的詳細資訊,程式寫的這麼複雜是因為我採用了mysqli的驅動,以便能使用到 multi_query 方法來支援同時執行多條SQL語句,這樣能更好的說明SQL注入攻擊的危害性。

假設我們可以通過 http://localhost/test/userinfo.php?username=plhwin 這個URL來訪問到具體某個會員的詳情,正常情況下,如果瀏覽器裡傳入的username是合法的,那麼SQL語句會執行:

SELECT uid,username FROMuserWHERE username='plhwin'
但是,如果使用者在瀏覽器裡把傳入的username引數變為 plhwin';SHOW TABLES-- hack,也就是當URL變為 http://localhost/test/userinfo.php?username=plhwin';SHOW TABLES-- hack 的時候,此時我們程式實際執行的SQL語句變成了:


SELECT uid,username FROMuserWHERE username='plhwin';SHOWTABLES-- 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,程式碼如下:

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'
',print_r($userinfo, 1),'
'</span>;</span>
    } else {
echo"使用者名稱不存在或密碼錯誤!";
    }
}
?>



    "utf-8"></span>

</head>

"LOGIN_FORM"
 method="post" action=""></span>
    登入帳號: "text" name="username" value="" size=30 />

</span>
    登入密碼: "text" name="password" value="" size=30 />

</span>
    "submit" value="登入" /></span>
    </form>
</span>
</span>
</pre></td></tr></tbody></table></figure>
此時如果輸入正確的使用者名稱 plhwin 和密碼 123456,執行的SQL語句為:

SELECT uid,username FROMuserWHERE username='plhwin'ANDpassword='e10adc3949ba59abbe56e057f20f883e'

上面語句沒有任何問題,可以看到頁面打印出了登入成功後的會員資訊,但如果有搗蛋鬼輸入的使用者名稱為 plhwin' AND 1=1-- hack,密碼隨意輸入,比如aaaaaa,那麼拼接之後的SQL查詢語句就變成了如下內容:


SELECT uid,username FROMuserWHERE username='plhwin'AND1=1-- hack' AND password='0b4e7a0e5fe84ad35fb5f95b9ceeac79'
執行上面的SQL語句,因為1=1是永遠成立的條件,這意味著黑客只需要知道別人的會員名,無需知道密碼就能順利登入到系統。

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

1、錯誤提示

如果目標Web網站開啟了錯誤顯示,攻擊者就可以通過反覆調整發送的引數、檢視頁面列印的錯誤資訊,推測出Web網站使用的資料庫和開發語言等重要資訊。

2、盲注

除非運維人員疏忽,否則大部分的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安全領域的各種攻擊方式,大多數都是因為開發者違反了這個原則而導致的,所以自然能想到的,就是從變數的檢測、過濾、驗證下手,確保變數是開發者所預想的。

1、檢查變數資料型別和格式

如果你的SQL語句是類似where id={$id}這種形式,資料庫裡所有的id都是數字,那麼就應該在SQL被執行前,檢查確保變數id是int型別;如果是接受郵箱,那就應該檢查並嚴格確保變數一定是郵箱的格式,其他的型別比如日期、時間等也是一個道理。總結起來:只要是有固定格式的變數,在SQL語句執行前,應該嚴格按照固定格式去檢查,確保變數是我們預想的格式,這樣很大程度上可以避免SQL注入攻擊。

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

2、過濾特殊符號

對於無法確定固定格式的變數,一定要進行特殊符號過濾或轉義處理。以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注入呢?

3、繫結變數,使用預編譯語句

MySQL的mysqli驅動提供了預編譯語句的支援,不同的程式語言,都分別有使用預編譯語句的方法,我們這裡仍然以PHP為例,編寫userinfo2.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'
',print_r($userinfo, 1),'
'</span>;</span>
</pre></span>
從上面的程式碼可以看到,我們程式裡並沒有使用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開發中,傳統的加解密大致可以分為三種:

1、對稱加密:即加密方和解密方都使用相同的加密演算法和金鑰,這種方案的金鑰的儲存非常關鍵,因為演算法是公開的,而金鑰是保密的,一旦密匙洩露,黑客仍然可以輕易解密。常見的對稱加密演算法有:AES、DES等。

2、非對稱加密:即使用不同的金鑰來進行加解密,金鑰被分為公鑰和私鑰,用私鑰加密的資料必須使用公鑰來解密,同樣用公鑰加密的資料必須用對應的私鑰來解密,常見的非對稱加密演算法有:RSA等。

3、不可逆加密:利用雜湊演算法使資料加密之後無法解密回原資料,這樣的雜湊演算法常用的有:md5、SHA-1等。

在我們上面登入系統的示例程式碼中,$md5password = md5($password);從這句程式碼可以看到採用了md5的不可逆加密演算法來儲存密碼,這也是多年來業界常用的密碼加密演算法,但是這仍然不安全。為什麼呢?

這是因為md5加密有一個特點:同樣的字串經過md5雜湊計算之後生成的加密字串也是相同的,由於業界採用這種加密的方式由來已久,黑客們也準備了自己強大的md5彩虹表來逆向匹配加密前的字串,這種用於逆向反推MD5加密的彩虹表在網際網路上隨處可見,在Google裡使用md5 解密作為關鍵詞搜尋,一下就能找到md5線上破解網站,把我們插入使用者資料時候的MD5加密字串e10adc3949ba59abbe56e057f20f883e填入進去,瞬間就能得到加密前的密碼:123456。當然也並不是每一個都能成功,但可以肯定的是,這個彩虹表會越來越完善。

所以,我們有迫切的需求採用更好的方法對密碼資料進行不可逆加密,通常的做法是為每個使用者確定不同的密碼加鹽(salt)後,再混合使用者的真實密碼進行md5加密,如以下程式碼:


//使用者註冊時候設定的password
$password = $_POST['password'];
//md5加密,傳統做法直接將加密後的字串存入資料庫,但這不夠,我們繼續改良
$passwordmd5 = md5($password);
//為使用者生成不同的密碼鹽,演算法可以根據自己業務的需要而不同
$salt = substr(uniqid(rand()), -6);
//新的加密字串包含了密碼鹽
$passwordmd5 = md5($passwordmd5.$salt);
</span>
小結
1、不要隨意開啟生產環境中Webserver的錯誤顯示。
2、永遠不要信任來自使用者端的變數輸入,有固定格式的變數一定要嚴格檢查對應的格式,沒有固定格式的變數需要對引號等特殊字元進行必要的過濾轉義。
3、使用預編譯繫結變數的SQL語句。
4、做好資料庫帳號許可權管理。
5、嚴格加密處理使用者的機密資訊。