1.网络应用和SQL注射
1.1概述
有些网络数据库没有过滤客户提供的数据中可能有害的字符,SQL注射就是利用插入有害字符进行攻击的技术。尽管非常容易防范,但因特网上仍然有惊人数量的存储系统容易受到这种攻击。这篇文章的目的是指导专业安全组织了解这种技术,并告诉他们正确的,用来防范SQL注射的办法,以及处理各种常见的,由于非法输入引起的问题.
1.2背景
在读这篇文章之前,你应该对数据库如何工作,以及SQL如何被用来访问数据库有一些基础的了解。我建议您阅读eXtropia.com的文章“Introduction to Databases for WebDevelopers”。
1.3字符编码
在大多数的网络浏览器中,标点符号和许多其它符号在用于一个网络请求前需要把URL编码,以便被适当地编译(interpret)。在本文中的例子和截图中我使用了固定的ASCII字符以保证最大的可读性。然而,在实际应用中,你需要在HTTP请求中用%25来代替百分号(%),用%2B来代替加号(+)等等。
2.易损性的测试(Testing for vulnerability)
2.1综合测试
彻底地检测一个网络请求是否容易被SQL注射比一个可能的猜测(might guess)需要耗费更多的精力。当你把一个单引号放进一个脚本的第一个参数值时,服务器返回一个空白的网页,上面除了ODBC错误以外什么都没有.显然这种情况直接反映出web程序存在漏洞,但通常都不是这样的,如果你没有注意细节的话,很容易忽略掉一个看上去完美,其实很脆弱的脚本。
服务器上每一个脚本中的每一个参数都应该被检测。开发者和开发组织之间可能很不一致。设计脚本A的程序员也许和脚本B的开发毫无关系,所以,其中一个也许对SQL注射免疫,而另外一个可能不会。事实上,设计脚本A里的函数A的程序员也许和脚本A里的函数B的开发毫无关系,所以脚本A里的一个参数也许对SQL注射是脆弱的,而另外一个参数却不一定,即使整个网络请求是由一个程序员来构想,设计,编写及测试的,在成千上万的脚本中的参数中,由于某种原因,设计者忘了检验某个地方的数据,所以仍有可能存在一个脆弱的参数,而且那个地方是唯一的,你永远都不能确定是哪里,所以必须测试所有的东西。
2.2测试过程
用一个单引号和一个SQL关键字(比如“WHERE”)替代每一个参数的值(argument),每个参数都应该被单独地测试,不止那样,当你测试一个参数的时候,应该保持其它的参数不变,并用有效的数据填充它们的值(argument),It can be tempting to just delete all of the stuff that you're not working with in order to make things look simpler, particularly with applications that have parameter lines that go into many thousands of characters.
当你测试一个参数是否能被SQL注射的时候,如果忽略了其它参数或者给他们一个错误的值(argument),网络请求就有可能由于其它原因而出错,这阻碍了你判断SQL注射是否可行。比如,让我们假设以下是一个有效的,纯粹的(unaltered)参数行:
ContactName=Maria%20Anders&CompanyName=Alfreds%20Futterkiste
并且它返回一个ODBC错误:
ContactName=Maria%20Anders&CompanyName='%20OR
如果我们这样检测:
CompanyName='
可能只会给你一个错误告诉你需要指定一个ContactName值。
这行:
ContactName=BadContactName&CompanyName='
可能返回同样的页面,因为请求根本没有指定ContactName。或者,它可能返回你站点默认的主页。或者,可能它找不到指定的ContactName,或者web程序认为没有必要看CompanyName,所以它甚至根本不把这个参数值认为是一个SQL声明,或者,它可能给你一些完全不同的东西,所以,当检测SQL注射的时候,记得总是用完整的参数行,并且除了你正在检测的那个参数外,还要给其它所有的参数一个合法的值。
2.3分析结果
如果你得到一个数据库服务器返回的某些错误信息,那么SQL注射显然是存在的.然而,数据库错误信息不一定总是明显的(有时候编写程序的人可能做一些奇怪的事情),所以,你应该顺便看看每个可能的地方来确认注射是否成功,首先你应该从返回的页面上的所有资源中找寻像"ODBC", "SQL Server", "Syntax"等的短语,更多的信息可能含在HTTP的头部,隐藏的输入...。我曾见过某些存储系统上的网络请求返回的错误信息中,在HTTP回复的body中完全没有任何信息,但在头部中却有数据库错误信息。为了调试和QA的目的,很多网络请求都内嵌了这种特征,然而到最后发表前却忘了把它们去处掉或使之无效。
你不只要注意即时返回的页面,同样链接页面也要看,在最近的一次pen-test中,我看到一个网络请求被SQL注射攻击后,返回了一个类错误信息页面,点击错误旁边的停止标志图片,链接到了另外一个满是SQL服务器错误信息的页面。
另一个应该密切注意的是302页面重定向,在你有机会注意到它之前,你可能就无奈的离开了一个含有数据库错误信息的页面.
请注意即使你真的得到了一个ODBC错误信息回复,SQL注射仍有可能成功,很多时候(Lots of the time)你得到一个properly formatted, seemingly类错误消息页面,告诉你"an internal server error" 或者 "problem processing your request."
有些网络请求被设计成一旦出现任何的错误,客户都返回到站点的主页面。如果你得到一个500错误页面,很有可能注射就出现了,很多站点都有一个默认的500服务器内部错误页面来说明服务器正在维护中,或礼貌的让用户把他们的请求email给站点的维护人员。这就有可能用procedure techniques来利用这些站点,这将在后面讨论。
3.1绕过验证
最简单的SQL注射技术是绕过基于表单的登陆.让我们假设某个网络请求的代码如下:
SQLQuery = "SELECT Username
FROM Users WHERE Username = '" & strUsername & "'
AND Password = '" & strPassword & "'"
strAuthCheck = GetQueryResult(SQLQuery)
If strAuthCheck = "" Then
boolAuthenticated = False
Else
boolAuthenticated = True
End If |
当一个用户提交了一个用户名和密码后,查询(query)将搜索Users表单来看是否其中有一行中所包含的用户名和密码与用户提供的相同,如果找到了那么一行,则用户名被储存到变量strAuthCheck中,同时说明该用户应该被鉴定,如果没有找到那么一行,则strAuthCheck变量保持为空,同时该用户不被鉴定。
如果strUsername和strPassword变量可以包含任何你要的字符,你可以修改当前的SQL查询结构,那样即使你不知道有效的用户名和密码,你仍何以得到一个有效的name,它是如何实现的呢?让我们假设用户像下面那样填充了一个登陆表单:
Login: ' OR ''='
Password: ' OR ''=' |
这将给SQLQuery以下值:
SELECT Username FROM Users WHERE Username = '' OR ''='' AND Password = '' OR ''=''
请求并不把用户提交的数据与现存的Users表单做比较,而是直接比较''和'',显然它总是返回true,(注意nothing和null是有区别的)由于WHERE语句中的所有验证条件都符合了,用户名将使用表单中搜索到的第一行中的那个,接着用户名将被传递给变量strAuthCheck,这样我们的效力就得以保证。使用single result cycling技术,也有可能使用另外一行的数据,这将在以后讨论。
3.2 SELECT
对于另一些情况而言,你必须根据查询那些有缺陷的web程序返回的结果,来判断和调整你提交的SQL查询字符串,以便搞定服务器.
3.2.1 直接利用单引号
你将面临的第一个错误是语句结构错误.一个结构错误表明SQL查询的语句结构存在缺陷.首先你应该明白,在没有编码引号的情况下, 插入脚本攻击是否可以成功.
直接SQL注射的时候,无论你提交什么语句都会被不加任何改变地应用于SQL查询中.试着提交参数的时候,先输入合法的值,然后在其后添加一个空格和一个OR,如果服务器产生了错误,那么直接SQL注射是可能的.提交的值可以是任何WHERE子句中用到的值,例如:
SQLString = "SELECT FirstName, LastName,
Title FROM Employees WHERE Employee = " & intEmployeeID |
或者是紧跟于一个SQL关键字,例如表名或者表里的栏目名,比如
SQLString = "SELECT FirstName, LastName,
Title FROM Employees ORDER BY " & strColumn |
所有其他的例子都是引号注射,在一个存在引号插入漏洞的程序里面, 任何一个你提交的参数,系统都会在前面和后面添加一个引号,就像这样:
SQLString = "SELECT FirstName, LastName,
Title FROM Employees WHERE EmployeeID = '" & strCity & "'" |
为了能(break out)打破这引号,并伪造一个正确的查询,在你的SQL注射字符串中的SQL关键字之前必须包含一个单引号,而且在WHERE子句的后面也需要加上一个单引号.现在我们来谈谈"欺骗"的问题.是的,SQL SERVER会忽视在";--"后面的任何东西,但是只有MS的SQL SERVER会这样做.我们最好学习如何处理这个问题,这样我们在面对Oracle,DB/2,MySQL 和他种类的数据库服务器的时候就知道怎么做了.
SELECT查询被用于从数据库中获取信息.大多数的web应用程序通过SELECT向数据库获取信息候再动态地在页面上显示出来.通常,数据库查询这部分你可以自己伪造,他将成为WHERE子句的一部分.我们可以通过插入UNION SELECT来绕过web程序允许我们查询的数据,从而得到其它的数据.联合查询(指UNION SELECT)允许在一条语句中使用多个SELECT查询,看上去就像这样:
SELECT CompanyName FROM Shippers
WHERE 1 = 1 UNION ALL SELECT CompanyName FROM Customers WHERE 1 = 1 |
它返回的结果中包含了第一个查询和第二个查询的结果,"ALL SELECT"这里的ALL是必须的,这样可以逃过SELECT DISTINCT语句的限制并且不会妨碍别的(??),所以最好是使用它.你必须确认第一个查询,即web应用程序编写者希望执行的那个被执行,不返回任何记录.这并不难.举个例子,有这么一个表达式:
SQLString = "SELECT FirstName,
LastName, Title FROM Employees WHERE City = '" & strCity & "'" |
我们构造如下的插入串:
' UNION ALL SELECT OtherField FROM OtherTable WHERE ''=' |
这将导致如下的SQL查询语句被提交给SQL SERVER:
SELECT FirstName, LastName,
Title FROM Employees WHERE City = ''
UNION ALL SELECT OtherField FROM OtherTable WHERE ''='' |
让我们看看会发生什么:数据库搜索Employees表,查找City被设置为NULL的那一行,由于它找不到哪一行city是NULL,所以它不会返回任何记录,只有我们inject的查询才会返回记录.在一些情况下,使用NULL不能成功,因为表里的却存在
有NULL的项.在这种情况下,你要做的就是构造一个表中不存在的值,你只要输入一些不普通的值...最好是对照那些正常的值,当数据库需要一个自然数时,0或者负数都工作得很好,对于一个文本参数,简单的用"NoSuchRecord","NotInTable"或更常见的"sjdajdhajsh",只要它不返回记录就好.
如果所有的web应用程序使用的SQL查询都像上面这些那么简单就好了,可惜这不可能: ].按照各个编程者习惯和查询表达式编写方式的不同,你SQL注射时可能会遇到各种困难.