5.限制输入长度
如果在Web页面上使用文本框收集用户输入的数据,使用文本框的MaxLength属性来限制用户输入过长的字符也是一个很好的方法,因为用户的输入不够长,也就减少了贴入大量脚本的可能性。程序员可以针对需要收集的数据类型作出一个相应的限制策略。
6.URL重写技术
我们利用URL重写技术过滤一些SQL注入字符,从而达到防御SQL注入。因为许多SQL注入是从URL输入发生的。
7.传递参数尽量不是字符
假设我们显示一篇新闻的页面,从URL传递参数中获得newid我们可能会随手写下下面的代码:
string newsid = Request.QueryString["newsid"];
string newssql = "select * from news where newsid=" + newsid;
如果传递过来的参数是数字字符就没有问题。但是如果传递过来的newsid是“1 delete from table ”的话,那么sql的值就变成了“select * from table where newsid=1 delete from news”。发生注入成功。但是这里改为
int newsid=int.Parse(Request.QueryString["newsid"].ToString());
string newssql = "select * from news where newsid=" + newsid.Tostring();
这里如果还是上面"1 delete from table "会发生错误,因为在转换时候出现了错误
从上面的一个小例子,我们得出在传递参数时候尽量不要用字符,免得被注入。
最后是我想扩展下利用URL重写技术来过滤一些SQL注入字符,首先这里有一篇关于URL重写的文章,我的基本思想是可以利用它,屏蔽一些危险的SQL注入字符串,这些字符串我们可以人为的设定,毕竟我们还是根据特定的环境设定我们防御措施。首先我们在ModuleRewriter类中的Rewrite函数得到绝对的URL判断其中是否有危险字符,如果有我们就将它链接到一个提示用户您输入危险的URL地址。如果不是我们继续判断是否触发了其他的URL重写的规则,触发了就重写。这样就大致上能在URL上防御危险字符
上面是要在web.config配置文件加上的内容,这里我加上了两个重写规则,第一个规则是专门针对满足这个正则表达式的页面URL查看是否有危险字符,有危险字符就会发送到Default_sql_error.aspx页面,来示警。这里我假定会发生危险字符注入的页面时以"d"字符开头并加上数字的页面(这里我们可以根据实际情况自己定义,看哪些页面URL容易发生我们就制定这些页面的正则表达式),第二个是一般URL重写。因为这里我采用的是HTTP模块执行URL重写,所以加上<httpModules></httpModules>这一块。
第二步就是要在重写Rewrite函数了
{
// 获得配置规则
RewriterRuleCollection rules = RewriterConfiguration.GetConfig().Rules;
Uri url = app.Request.Url;
// 判断url 中是否含有SQL 注入攻击敏感的字符或字符串,如果存在,sqlatFlag = 1 ;
string urlstr = url.AbsoluteUri;
int sqlatFlag = 0;
string words = "exec ,xp ,sp ,declare ,cmd ,Union ,--";
// 如果还有其他敏感的字符或者符号,可以加入上面这行字符串中
string[] split = words.Split(',');
foreach (string s in split)
{
if (urlstr.IndexOf(s.ToUpper()) > 0)
{
sqlatFlag = 1;
break;
}
}
if (sqlatFlag == 1)
{
// 创建regex
Regex re = new Regex(rules[0].SendTo, RegexOptions.IgnoreCase);
// 找到匹配的规则,进行必要的替换
string sendToUrl = RewriterUtils.ResolveUrl(app.Context.Request.ApplicationPath, re.ToString());
// 重写URL
RewriterUtils.RewriteUrl(app.Context, sendToUrl);
//RewriterUtils.RewriteUrl(app.Context, rules[0].SendTo);
}
else
{
// 遍历除rules[0 ]以外的其他URL 重写规则
for (int i = 1; i < rules.Count; i++)
{
// 获得要查找的模式,并且解析URL (转换为相应的目录)
string lookFor = "^" + RewriterUtils.ResolveUrl(app.Context.Request.ApplicationPath, rules[i].LookFor) + "$";
// 创建regex
Regex re = new Regex(lookFor, RegexOptions.IgnoreCase);
// 查看是否找到了匹配的规则
if (re.IsMatch(requestedPath))
{
// 找到了匹配的规则, 进行必要的替换
string sendToUrl = RewriterUtils.ResolveUrl(app.Context.Request.ApplicationPath, re.Replace(requestedPath, rules[i].SendTo));
// 重写URL
RewriterUtils.RewriteUrl(app.Context, sendToUrl);
break;
// 退出For 循环
}
}
}
}
那么下一步就是检验例子了
首先我们输入http://localhost:4563/web/Default.aspx?id=1;--
这样http://localhost:4563/web/Default.aspx?id=1;-- 没有改变,就会显示Default_sql_error.aspx里内容“您输入了危险字符”。
再输入http://localhost:4563/web/D11.aspx就会显示 Default2.aspx内容,因为这里触发了第二个重写规则
试验成功。
当然用URL防SQL只是我想到一种怪癖思路,大家如果有什么特别方法可以共同考论一下,还是引用前辈们话“见招拆招”遇到实际的问题我们就根据实际情况解决,选用那个最实用的方法。
一个客户对我们请求说,请我们来检查一下他的内部网络,这个网络被公司的职员以及客户们来使用。这是一个较大的安全评估的一部分,而且,虽然我们以前从没 有真正的使用过SQL注入来破解一个网络,但是我们对于其一般的概念相当的熟悉。在此次“战斗”中,我们是完全成功的,而且想要通过把这个过程的每一个步骤重新记录下来,并作为一个“生动的例子”。
“SQL注入”是特定的一种未被确认或未明确身份的用户输入漏洞的一个子集(“缓冲溢出”是一个不同的子集),而这个想法的目标是,让应用程序确信从而去 运行SQL代码,而这些代码并不在其目的之内。如果一个应用程序是在本地通过即时的方式来创建一个SQL字符串,结果很直接,会造成一些真正的出人意料的结果。
我们要明确说明的是,这是一个有些曲折的过程,并且其中会有多次的错误的转折,而其余的更有经验的人当然会有着不同的-甚至更好的-方法。而事实上,我们成功的实现了建议,并没有被完全的误导。
还有一些不同的论文讨论SQL注入问题,包括一些更加详细的文章,不过此文所展示的,与破解的过程同样份量的是发现了SQL注入的原因。
目标内网
这显然是一个完全自主开发的应用程序,而我们对于它没有预先的了解,或者访问源代码的权限:这已是一个“blind”攻击。若干次侦测之后,我们了解到服 务器运行的是微软的IIS6,并使用ASP.NET框架,从这其中得到的,似乎可以假定数据库是微软的SQL服务器:我们相信这些技术可以应用于几乎任何 一种web应用,而此应用可能为任何一种SQL服务器所支持。
登陆页面是一个传统的用户名-密码表单,带有一个用电子邮件给我传送密码的链接;而后者被证实是整个系统的败笔。
当输入一个电子邮件地址的时候,系统会假定次邮件存在的方式,再用户数据库里面寻找这个电子邮件地址,并且会邮寄一些内容到这个地址。由于我的电子邮件地址没有被找到,所以它不会给我发送任何内容。
所以,第一个测试,对于任何SQL化的表单而言,是输入一个带有单引号构成的数据:这样做的目标是查看是否他们在构建SQL字符串的时候根本没有使用数据 的清理机制。当为此表单提交了一个有单引号的电子邮件之后,我们得到了一个500错误(服务器失败),这就是说,这个“被破坏了的”的输入实际上被真实的 分析过。中!
我们猜测底层的SQL代码可能类似于如此:
- SELECT fieldlist FROM table WHERE field = '$EMAIL';
这里,$EMAIL是由用户通过表单提交的电子邮件地址,而这段较长的查询提供的应用符号,是为了使得这个$EMAIL成为一个真正的字符串。我们不知道 这个数据域的确切的名字或者是于此相关的数据表的名字,但是我们却了解他们的特性,而此后我们将会得到一些很好的猜测结果。
当我们输入steve@unixwiz.net' - 留意那个结尾的引号 - 这将产生出一个如下构建的SQL:
- SELECT fieldlist FROM table WHERE field = 'steve@unixwiz.net'';
当这个SQL被执行的时候,SQL分析器发现了多余的引号,从而中止了工作,并给出一个语法错误。而这个错误如何表述给用户,取决于应用程序内部错误修复 的过程,但是这通常不同于“电子邮件地址不存在”的错误提示。这个错误的响应是致命的第一通道,既用户的输入没有被正确的清理,而因此应用程序成为了破解 的美食。
由于我们输入的数据显然位于WHERE子语句中,让我们用合法的SQL方式改变一下这个子句的本貌并看看会发生什么。通过输入任何一种‘OR ’x‘=x语句,结果SQL成为:
- SELECT fieldlist FROM table WHERE field = 'anything' OR 'x'='x';
因为应用程序没有真正的对这样的查询有所考虑,而仅仅是构建一个字符串,以致于我们使用的引号使得一个单元素的WHERE子句,变成了一个双元素的子句, 而且’x'=x字句是确定为真的,无论第一个字句是什么。(有一种更好的方式来确保“始终为真”,这部分我们后面会谈及)
不过与“真实”的查询不同,本应当一次返回一个单独的项,这个版本必然会返回成员数据库里面的每一个项。唯一的可以发现应用程序在这种情况下会做什么的方式,就是尝试。不断尝试,我们留意到以下结果:
你的登录信息已经发送到 random.person@example.com.
我们一般都会把查询返回的第一行作为作为猜测的主要入口。这哥们确实从E-mail里拿回了他的密码,同样这个邮件可能会让他感到吃惊并且会引起怀疑。
现在我们知道怎么在本地玩这条查询了,虽然目前我们还不知道我们看不到的那部分SQL结构是怎么拼起来的。但是我们通过逆向工程看到了三个不同的查询结果:
- 您的登陆信息已经以Email形式发送给您
- 我们无法识别您的Email地址
- 服务端错误
头两个响应是有效的查询的结果,最后一个是无效SQL造成的。 类似这样的响应结果会帮助我们更好的逆推服务端用来查询的SQL语句结构。
预设字段映射
我们要干的第一步就是猜字段名,首先我们合理的推测查询带了“email address” 和 “password”,所以可能的字段名选择会有“US Mail Address” 或者 “userid” 亦或者“phone number” 。当然最好能执行 show table,但是我们又不知道表名,貌似目前没什么明显的方法能让我们拿到表名。
那就分步走吧。在每个例子里,我们会用我们已知的SQL加上我们自己的特殊“段”。我们已知的这条SQL的结尾是个Email地址的比对,那就猜下email是这个字段名吧
- SELECT fieldlist FROM table WHERE field = 'x' AND email IS NULL; --';
如果服务器响应是报错,那基本上可以说明我们的SQL拼错了。但是如果我们得到任何正常的返回,例如“未知的邮件地址” 或 “密码已发送” ,说明我们的字段名蒙对了。
要注意的是,我们的“And” 关键字而没用“OR” 关键字,这么做是有目地滴。在上一步中,我们不关心到底是哪一个Email,而且我们不想因为蒙中某人的Email然后给他发了重置密码的邮件。这么搞那 哥们儿一定会怀疑有人对他的帐号搞三搞四。所以用“And”关键字拼上一个不合法的Email地址,这样服务端总是返回空结果集,也就不会给任何人发邮 件。
提交上面的SQL代码段确实返回了“未知的邮件地址” 这么一个响应。现在我们确认了email地址的字段名是email。如果不是这么个响应,那我就再蒙“email_adress”或者“mail”亦或者 其他类似的。这个环节总是靠蒙的,但是蒙也得讲技巧和方法方式。
这段SQL的用意是我们假设预设的SQL查询中的字段名是 email ,跑一下看看是不是有效。我不会管你到底有没有匹配的Email,所以用了个伪名“x” , “--” 这个标示是表示SQL的起始。这样SQL解析到这就会把它直接当成一条命令,而“--”后面的会是一个新的命令,这样就屏蔽掉了后面的那些不知道的玩意 儿。
下一步,我们来猜下其他比较明显的字段名: "password","userid","name" 和类似的。每次我们只蒙一个名字,当返回结果不是“服务端错误” , 那就说明我们蒙对了。
- SELECT fieldlist FROM table WHERE email = 'x' AND userid IS NULL; --';
通过这一步,我们蒙出来了下面几个字段名:
- passwd
- login_id
- full_name
肯定还有更多其他的(把HTML页面表单的 <Input name="XXXXX"> 拿来做参考是个非常不错的选择)后来我又挖了一下但是没挖出来更多的字段名。 到目前为止,我们还是不知道这些字段所属的表的表名--咋个弄呢?
搜寻表名
应用内建的query已经把表名放在语句中,但我们不知道表的名字。有几种方法可以找到这些插入在语句中表名(以及其他的表名)。我们用的是一种依赖于subselect的方法。
下面这个单独的query
- SELECT COUNT(*) FROM <i>tabname</i>
返回表中记录的数量,当然,若表名是无效的,则查询失败。我们可以把这个查询放入我们的查询语句中来探查表名。
- SELECT email, passwd, login_id, full_name
- FROM <i>table</i> WHERE <b>email</b> = '<span>x' AND 1=(SELECT COUNT(*) FROM <i>tabname</i>); --'</span>;
我们实际上并不关心表中有多少记录,我们关心的是表名是否有效。通过试探不同的猜测,我们最终确定members是数据库中的有效表名。但是,这是这个查 询中所用的表名吗?为此,我们需要另一个使用table.field的查询:这只在表名的确是查询中的表名时才工作,而不仅仅当表存在时工作。
- SELECT email, passwd, login_id, full_name
- FROM members
- WHERE email = '<span>x' AND members.email IS NULL; --</span>';
当这个语句返回 "Email unknown"时,可以确认我们的SQL正确执行了,并且我们成功的猜出了表名。这对后面的工作很重要,但我们先临时使用一下另一种方法。
弄几个帐号先
目前我们搞到了members表结构的部分信息,但是我们只知道一个用户名,就是之前我们蒙中的那个发了邮件通知的那个用户名。当时我们只得到了邮件地址,但是拿不到邮件内容。所以我们得再弄几个有效得用户名,高端洋气上档次的最好。
我们的从公司的网站开始人肉,找到那些人物介绍的页面,一般都是介绍公司内部人员的。这些介绍里大多都有这些人的Email地址和名字。就算没有这些信息也没啥,咱兜里还有货。
思路是这样的,提交一个带有“Like” 关键字的SQL,这样我们可以对Email地址或者用户名做些模糊匹配,每次提交如果返回“我们已发送您的密码至邮箱”那也就是说我们的模糊查询有效了,而且邮件也真的发了!!!。这么干虽然我们能拿到邮件地址,也意味着对方会收到邮件并引起警觉,所以 慎用!!
我们能做email、full_name(或者其他字段)的查询,每次放入%这个通配符执行如下的查询:
- SELECT email, passwd, login_id, full_name
- FROM members
- WHERE email = 'x' OR full_name LIKE '%Bob%';
暴力破解密码
我们肯定可以在登陆页面尝试暴力破解密码,但是大多的应用都做了相应的防护手段。可能的防护会有操作日志,帐号锁定或者其他能大大降低我们效率的手段或设备,但是因为输入没有被过滤所以给我们绕过这些防护多了一些可能。
我们将把密码和邮件名称的代码段加到我们已知的SQL里。在这个例子里我们会用一个倒霉催的哥们的邮箱,bob@example.com 然后试试我们准备的一些密码。
- SELECT email, passwd, login_id, full_name
- FROM members
- WHERE email = 'bob@example.com' AND passwd = 'hello123';
这条SQL是完整有效的,所以服务器铁定不能够报错,所以我们知道当服务器响应是“您的密码已经发送至您的邮箱” 这么个结果是我们就知道刚提交的那个密码就是我们要的密码。虽然倒霉催的Bob也收到了邮件并且一定会警觉,但是我们在他警觉之前就干完我们想干的了。
这个过程可以在Perl下用脚本自动化完成,所以我们就去搞了搞Perl的脚本,结果写脚本的时候发现了另外一种方法来干这事。
数据库不是只读的
迄今为止,我们对数据库除了进行查询外,没做其他事。尽管SELECT是只读的,但不意味SQL就只能这样。SQL使用分号来中断一个语句, 如果对输入没有做正确的处理, 它就不能阻止我们在查询语句后面添加不相关的字符串。
最恰当的一个例子就是这样:
- SELECT email, passwd, login_id, full_name
- FROM members
- WHERE email = 'x'; DROP TABLE members; --'; -- Boom!
第一部分提供了一个假的邮件地址 -- 'x' -- 我们并不关系这个查询的返回,我们仅仅是给出了一个我们能够使用无关SQL指令的方式,一个尝试删除整个members表而真的是无任何关系的操作.
这表明我们不仅仅可以切分SQL指令,我们还可以修改数据库,这完全是被允许的。
加入一个新成员
由上所得,我们获知了members表的部分的结构,尝试添加一个新的记录到这个表里面似乎是一个可拊掌称庆的方法:如果这能成功,我们就能够通过我们新插入的帐户来直接登陆了。
这里,毫不惊奇的是,这只需要稍微加些SQL,我们把它放在不同的行从而让我们的展示更易理解,不过从头到尾这一部分其实仍然是一个字符串:
SELECT email, passwd, login_id, full_name
FROM members
WHERE email = 'x'; INSERT INTO members ('email','passwd','login_id','full_name') VALUES ('steve@unixwiz.net','hello','steve','Steve Friedl');--';
即便是我们真的确定了表的名字以及使用的字段都是正确的,在成功的实施攻击之前,仍然有一些障碍:
- 可能,在表单上,我们并没有足够的空间来直接输入这些文本(尽管,可以使用脚本来解决这个问题,而这并不是很简便的事情)
- web应用程序的用户可能并没有在members表上的插入权限。
- 毫无疑问,members表里面有一些别的字段,有一些可能需要初始化的数据,而这可能导致插入失败。
- 即便是可以达到插入一个新的记录,应用程序可能会出现不当的行为,因为自动插入的时候有一些字段使用的是NULL。
- 一个有效的“成员”可能不仅仅需要在members表里面的一个记录,而是和别的表有着数据上的关联,(如,“访问权限”),所以添加一个数据到一个表中,可能并不充分有效。
对于手头的案例而言,我们遇到了#4或者是#5上面的障碍 - 我们无法真正确定是哪一个 - 因为在主登陆界面上,输入上面的用户名+密码的时候,返回了一个服务器上的错误。这就是说,那些我们没有用到的字段可能是必要的字段,而尽管如此,它们仍 然没有被正确的处理。
这里,一个可行的方法,就是猜测别的字段,但是这可以保证是一个长时间而且耗费劳力的过程:虽然你可能可以猜测出来那些“显而易见”的字段,但是却很难构建出整个应用程序的组织图像。
所以,最后我们选择走另一条不同的路。