SQL注入攻擊與防御實例
- 1.1
以下是一段普普通通的登錄演示代碼,該腳本需要username和password兩個參數,該腳本中sql語句沒有任何過濾,注入起來非常容易,后續部分將逐步加強代碼的防注入功能。
<?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注入的例子:
username='or''='
password='or''='
如果這樣的話SQL語句就變成了select * from users where username=''or''=' and password=''or''='' limit 1,顯然條件是個永真式,查詢一定成功。
或者
username='or''=' limit 1#
password=任意非空值
SQL語句可以自己寫一下。
除了上述的payload,還有很多其他的payload可用。
- 1.2
如何將上述代碼加強一下呢?上述代碼在進行查詢時同時查詢了username和password,查詢時用戶能操作的參數越多,不確定性就越大。可以換一種思路,查詢時拼接的字符串只用到主鍵username,后面在檢查password和數據庫中的是否一致。即,可以調整查詢的結構,減少用戶可控的參數拼接。
數據庫中密碼明文不太好,順便md5處理一下,加鹽效果更好,可以防止數據庫被黑了導致敏感信息泄漏。
$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''='顯然是不可以了,除非你知道數據庫中第一個用戶的密碼。但是畢竟還是可以破解,因此可以在借助過濾函數來幫忙。在這個例子中,由於username參數兩側是單引號,如果構造sql注入一定需要加入額外的單引號來破壞原語句,所以可以直接借助addslashes()函數將username中的單引號轉義。
$username = addslashes($_POST['username']);
$password = md5($_POST['password']);
在這個最簡單的例子中,經過這樣簡單的修改似乎已經沒有辦法注入了。后面會給一些其他的例子,並給出一些新方法來防御sql注入。
- 1.3
之前提到了過濾函數,用到的是PHP自帶的轉義函數,但是這個有時候是不夠用的。這種情況下可以自定義過濾函數。
常見的過濾手段就是限制關鍵字,通過正則實現。
以下是節選的某CTF賽題中的一段代碼,CTF中經常使用留有余地的過濾函數,讓選手可以進行SQL注入。
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