SQL注入攻擊與防護實例
- 1.1
如下是一段普普統統的登陸演示代碼,該腳本須要username和password兩個參數,該腳本中sql語句沒有任何過濾,注入起來很是容易,後續部分將逐步增強代碼的防注入功能。php
<?php include 'config.php'; $username = $_POST['username']; $password = $_POST['password']; if(!empty($username) && !empty($password)) { $conn = new mysqli($db_server,$db_user,$db_pass,$db_name); if(!$conn) die('數據庫鏈接失敗!<br/>'); $sql = "select * from users where username='{$username}' and password='{$password}'limit 1"; $result = $conn->query($sql); if($result->num_rows==1){ echo "<script>alert(\"登陸成功!\")</script>"; } else{ echo "<script>alert(\"用戶名或密碼錯誤!\")</script>"; } } else{ header("Location:/index.php?display=1"); } ?>
針對上面的代碼進行sql注入的例子:html
username='or''=' password='or''=' 若是這樣的話SQL語句就變成了select * from users where username=''or''=' and password=''or''='' limit 1,顯然條件是個永真式,查詢必定成功。 或者 username='or''=' limit 1# password=任意非空值 SQL語句能夠本身寫一下。
除了上述的payload,還有不少其餘的payload可用。mysql
- 1.2
如何將上述代碼增強一下呢?上述代碼在進行查詢時同時查詢了username和password,查詢時用戶能操做的參數越多,不肯定性就越大。能夠換一種思路,查詢時拼接的字符串只用到主鍵username,後面在檢查password和數據庫中的是否一致。即,<b>能夠調整查詢的結構,減小用戶可控的參數拼接</b>。程序員
數據庫中密碼明文不太好,順便md5處理一下,加鹽效果更好,能夠防止數據庫被黑了致使敏感信息泄漏。sql
$password = md5($_POST['password']); if(!empty($username) && !empty($password)) { $conn = new mysqli($db_server,$db_user,$db_pass,$db_name); if(!$conn) die('數據庫鏈接失敗!<br/>'); $sql = "select * from users where username='{$username}' limit 1"; $result = $conn->query($sql); if($result->num_rows==1){ $row = mysqli_fetch_assoc($result); if($row['password']==$password) echo "<script>alert(\"登陸成功!\")</script>"; else echo "<script>alert(\"用戶名或密碼錯誤!\")</script>"; } else{ echo "<script>alert(\"用戶名或密碼錯誤!\")</script>"; } }
這樣作的話若是繼續用username='or''='顯然是不能夠了,除非你知道數據庫中第一個用戶的密碼。可是畢竟仍是能夠破解,所以能夠在藉助<b>過濾函數</b>來幫忙。在這個例子中,因爲username參數兩側是單引號,若是構造sql注入必定須要加入額外的單引號來破壞原語句,因此能夠直接藉助addslashes()函數將username中的單引號轉義。數據庫
$username = addslashes($_POST['username']); $password = md5($_POST['password']);
在這個最簡單的例子中,通過這樣簡單的修改彷佛已經沒有辦法注入了。後面會給一些其餘的例子,並給出一些新方法來防護sql注入。函數
- 1.3
以前提到了過濾函數,用到的是PHP自帶的轉義函數,可是這個有時候是不夠用的。這種狀況下能夠自定義過濾函數。fetch
常見的過濾手段就是限制關鍵字,經過正則實現。url
如下是節選的某CTF賽題中的一段代碼,CTF中常用留有餘地的過濾函數,讓選手能夠進行SQL注入。spa
if(!empty($_POST["user_name"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); } if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); if(!$row) { echo 'error'; print_r($db->error); exit; } $msg = "<p>å§å:".$row['user_name']."</p><p>, çµè¯:".$row['phone']."</p><p>, å°å:".$row['address']."</p>"; } else { $msg = "æªæ¾å°è®¢å!"; } }else { $msg = "ä¿¡æ¯ä¸å ¨"; } ?>
該段代碼中限制了select,insert等不少關鍵字,對防止SQL注入有必定效果,可是有缺陷。若是考慮的不太全仍是會被注入,過濾函數設置的對關鍵詞過於敏感會讓不少正常信息的查詢也變得不易。
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
關於該題目的注入可參考如下文章,這裏用到了二次注入:
https://www.cnblogs.com/kevinbruce656/p/11347127.html#4355106
- 1.4
以前的各類方法都比較麻煩,對程序員不友好,有一種比較簡單的方法就是預編譯,既能有效的防止SQL注入,又容易編寫。
預編譯能防止SQL注入是由於SQL語句在執行前通過編譯後,數據庫將以參數化的形式進行查詢,當運行時動態地把參數傳給預處理語句時,即便參數裏有敏感字符如 'or''='數據庫也會將其做爲一個字段的屬性值來處理而不會做爲一個SQL指令。
總結一下,SQL注入的核心就是構造SQL指令,預編譯破壞了這個條件,所以能防止SQL注入。
舉個例子
<?php include 'config.php'; $username = $_POST['username']; $password = md5($_POST['password']); if(!empty($username) && !empty($password)) { $conn = new mysqli($db_server,$db_user,$db_pass,$db_name); if(!$conn) die('數據庫鏈接失敗!<br/>'); $sql = "select * from users where username=? limit 1"; $result = $conn->prepare($sql); $result->bind_param('s',$username); $result->bind_result($users,$pass); $result->execute(); if($result->fetch()){ if($pass==$password) echo "<script>alert(\"登陸成功!\")</script>"; else echo "<script>alert(\"用戶名或密碼錯誤!\")</script>"; } else{ echo "<script>alert(\"用戶名或密碼錯誤!\")</script>"; } $conn->close(); } else{ header("Location:/index.php?display=1"); } ?>
如下是比較核心的幾行
$sql = "select * from users where username=? limit 1"; $result = $conn->prepare($sql); $result->bind_param('s',$username); $result->bind_result($users,$pass); $result->execute();
第一行是一個SQL語句,?處須要被填充。
第二行是對SQL語句進行預編譯。
第三行是限制填充的類型爲字符串,使用username變量來填充SQL語句。
第四行是肯定查詢結果存儲到哪些變量中。
第五行是執行,執行完畢將會得到結果。
使用預編譯的方式防止SQL語句簡單有效,暫時沒有發現防不住的狀況,建議使用。
個人博客即將同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2k2m12zcg6nq