[Microsoft][ODBC SQL Server Driver][SQL Server]Incorrect syntax near ','.是怎么回事呢

这篇文章讨论常用的"sql注入"技术的细节,应用于流行的Ms IIS/ASP/SQL-Server平台。这里探讨有关这种攻击各种可以注入程序访问数据和数据库防范的方法。这篇文章面向两种读者:一是基于数据库web程序开发人员和审核各种web程序的安全专家。

结构化查询语言(SQL)是一种用来和数据库交互的文本语言SQL语言多种多样,大多的方言版本都共同宽松地遵循SQL-92标准(最新的ANSI标准[译者注:目前最新的是SQL-99])。SQL运行的典型的操作是“查询”,它是可以让数据库返回“查询结果记录集”的语句集合。SQL语句可以修改数据库的结构(用数据定义语言"DDL")和操作数据库里的数据(用数据操作语言"DML")。我们在这里着重讨论Transact-SQL(交互式SQL),应用于SQL-Server的SQL一种方言(非标准SQL)。如果攻击者可以插一系列的SQL语句进入应用程序的数据查询时,Sql注入攻击就可能发生。

似乎通过删除用户输入的字符串中的单引号或者通过一些方法避免它们出现可以解决这个问题。诚然如此,但是要实施这个解决方法还有很多的困难。因为首先:不是所有的用户提交的数据都是字符串形式,比如我们的用户输入通过'id'(看上去是个数字)来选择一个用户,我们的查询可能会这样:
在这种情况下攻击者可以轻易的在数值输入后面添加SQL语句。在其他SQL方言中,使用着各种分隔符,比如MS Jet DBMS引擎,日期可以用'#'符号来分隔。

其次,避免单引号并不像开始我们想象的那样是必要的解决办法,原因下面讨论。
我们将以Active Server Pages(ASP)登陆页面为例子来详细说明,它访问一个Sql-Server数据库并且验证一个到我们假想的程序的访问。

这是用户填写用户名和密码的表单页面:

如果用户指定了下面这样的数据:

'users'表会被删除,所有用户都不能登陆。'--'是Transact-SQL(交互式SQL)的单行注释符,';'标志着一个查询的结束另一个查询的开始。用户名最后的'--'用来使这个特殊的查询无错误结束。

攻击者只要知道用户名,就可以通过以下的输入以任何用户的身份登陆:

攻击者可以通过下面的输入以用户表里的第一个用户来登陆:

...更有甚者,攻击者通过以下的输入可以以任意虚构的用户登陆:

因为程序相信攻击者指定的常量是数据库返回的记录集的一部分。

[通过错误信息获取信息]

这个技术是David Litchfield在一次渗透入侵测试中首先发现的,后来david写了篇关于这个技术的文章,很多作者都参考过这篇作品。这里我们讨论“错误消息”技术潜在的机制,使读者可以充分理解它并且能灵活应用。

为了操作数据库里的数据,攻击者要确定某个数据库的结构。例如:我们的"user"表是用下面的语句建立的:

并且插入了下面的用户:

我们假设攻击者要为自己插入一个用户,如果不知道表的结构的话,他不可能成功。即使他运气好,'priv'字段的重要性还不清楚。攻击者可能插入'1',给自己在程序里添加了一个低权限的用户,而他的目标是管理员的权限。

对于攻击者来说幸运的是:如果程序返回错误(asp默认如此),攻击者可以猜测整个数据库的结构,读取ASP程序连接到SQL-Server的帐号权限内可以读取的任何值。

(下面给出的使用上面提供的示例数据库和asp脚本来说明这些技术怎样实现的)

首先,攻击者要确定查询的表名和字段名。要做到这点,攻击者可以使用'select'语句的'having'子句:

这会引起下面的错误(译者注:having字句必须和GROUP BY或者聚合函数一起配合使用,否则出错):

所以攻击者就知道了表名和第一列的列名,他们可以通过给每列加上'group by'子句继续得到其他列名,如下:

(结果产生这样的错误)

最后攻击者得到了下面的'username':

这句没有错误,相当于:

如果攻击者能确定各列的数据类型将会很有用,可以利用类型转换错误信息来达到这一点,看下面的例子:

这利用了SQL-Server试图在确定两行是否相同之前先执行'sum'子句的特性,计算文本域的和会返回这样的信息:

它告诉我们'username'字段的类型是'varchar'。相反的,如果我们试图计算数值型的字段,但结果两行的列数并不匹配:

我们可以用这个技术来大概地确定数据库内各列的类型。

这样攻击者就可以写出一个格式完美的'insert'语句:

但是,这个技术的潜力不止这些。攻击者可以利用任何错误信息来暴露系统环境或者数据库信息。执行下面的语句可以得到一个标准错误信息的清单:

检查这个清单可以发现很多有趣的信息。

一个特别有用的信息有关类型转换,如果你试图将一个字符串转换成整型,整个字符串的内容将会出现在错误信息里。以我们登陆页的例子来说,使用下面的'username'将会返回SQL-Server的版本以及它所在服务器操作系统的版本信息:

这试图将内置常量'@@version'转换成整型,因为'users'表第一列是整数。

这个技术可以用来读取任何数据库的任何表的任何内容,如果攻击者对用户名和密码感兴趣,他们就可以从'users'表读用户名:

这将选出比'a'大的最小用户名,而且试图将它转换成一个整数:

攻击者就知道'admin'帐号存在,他现在可以把他发现的用户名放进'where'子句来反复测试这行:

一旦攻击者确定了用户名,他就可以搜集密码;

一个更“别致”的技术是将用户名和密码连接成一个单独的字符传,然后试图将它转换成整型。这将举另一种例子;Transact-SQL语句可以将字符串连接成一行而不改变他们的意义,下面的脚本将连接这些值:

攻击者用这个'username'登陆(明显都在同一行)

这创建了一个只包含单列'ret'的表'foo',而且把我们的字符串放在里面。通常一个低权限的用户可以在示例数据库里创建表,或者一个临时表。

之后攻击者选择查询表里的字符串,就像前面说的:

这些例子仅仅揭开了这项技术的神秘面纱,不用说,如果攻击者可以从数据库获得丰富的错误信息,他们的工作将大大的简化。

一旦攻击者可以控制数据库,他们可能想通过这些权限来获得对网络更多的控制,可以通过很多方法来达到这一目的:

1.利用xp_cmdshell扩展存储以SQL-Server用户的身份在数据库服务器上执行命令
2.利用xp_regread扩展存储读取注册表的键值,也包括SAM(只要SQL-Server是以一个本地帐号运行的)
3.用其他的扩展存储改变服务器设置
4.在联合服务器上执行查询
7.用bcp在服务器上创建任何文本文件

这些只是常见的攻击方法的一部分;攻击者也很可能通过其他方法来达到目的,我们列举这些SQL-Server相关的攻击方法是为了说明如果程序可以被注入SQL语句时可能会发生什么,我们将依次给出以上各种情况的对策。

扩展存储的本质是编译了的动态链接库(DLLs),它用SQL-Server指定的调用方式去运行接口函数。他们允许SQL-Server程序拥有了和c/c++一样的功能,是个非常有用的特性。SQL-Server内置了大量的扩展存储,而且有各种各样的函数比如发送邮件和更改注册表。
xp_cmdshell是一个内置的扩展存储,它允许执行任意的命令行程序。例如:

将会获得一个SQL-Server进程所在工作目录的列表

将提供主机用户的列表。如果SQL Server正常的以本地'system'帐号或者'domain user'帐号运行,攻击者可以造成更严重破坏。

另外一个有用的内置的扩展存储是xp_regXXX函数

其中一些函数的用法的举例:

(它决定服务器的空连接式共享是否可用)

(它显示所有的服务器上SNMP公共的设置,通过这个信息,攻击者可以在相同的网络区域里重新配置网络设置,因为SNMP共有设置很少被改变而且由很多主机共享)

可以想象攻击者怎样利用这些函数来读取SAM文件,改变系统设置在重新启动后就被服务的应用,或者在用户下一次登陆时运行任意命令。

xp_servicecontrol扩展存储允许用户启动,停止,暂停或者运行服务。

下面是一些其他有用的扩展存储表:

SQL-Server提供了一个服务器联合的机制,就是允许一个数据库服务器上的查询操作其他服务器的数据。这些联合设置存放在master..sysservers表里,如果一个相连的服务器使用了'sp_addlinkedsrvlogin'存储过程,一个自动的登陆了的连接已经存在,可以通过它不登陆而访问该服务器。'openquery'函数允许查询在联合服务器上执行。

[用户自定义扩展存储]

扩展存储的API是相当简单的,创建一个带有恶意代码的扩展存储DLL也是相当容易的。通过命令行有很多方法将DLL上传到服务器,还有其他的很多方法包括各种通信机制来自动实现,比如HTTP下载和FTP脚本。
一旦DLL文件出现在服务器上SQL-Server可以访问,这不一定需要SQL-server本身,攻击者可以通过下面添加扩展存储(这里,我们的恶意扩展存储是个用来操作服务器的文件系统小的木马)

扩展存储就可以通过一般的方法调用:

一旦这个扩展存储执行过,可以这样删除它:

[向表中导入文本文件]

利用'bulk insert'语句,可以把一个文本文件的内容插入进一张临时表,我们简单的创建一个表:

然后执行bulk insert来插入数据来自于一个文件:

通过上面介绍过的错误信息技巧就可以得到数据,或者通过一个'union'查询,把文本数据作为查询的数据返回。这对于获得存储在数据库里的脚本如asp脚本很有用。

[利用BCP创建文本文件]

利用和'bulk insert'作用相反的技术创建任意的文本文件非常简单。不过需要一个命令行工具'bcp'('bulk copy program'),因为bcp在SQL-Server进程外访问数据库,它需要一次登陆。但是这不难,因为攻击者都可以创建一个;或者如果服务器配置使用了“完整性”安全模式,攻击者可以利用它。

'S'参数是要运行查询的服务器,'U'参数是用户名,'P'是密码,这里的密码是'foobar'。

SQL-Server提供了一些内置的扩展存储,允许在SQL-Server内创建ActiveX自动脚本。这些脚本在功能上和windows scripting host上运行的脚本或者asp脚本(通常用Javascript或者Vbscript编写)一样,脚本创建自动对象并且通过他们产生作用。一个用Transact-SQL写的自动脚本可以做任何asp脚本或者WSH脚本能做的事。

下面提供一些例子来说明:

1)这个例子用'wscript.shell'对象创建一个notepad的实例(当然这里也可以是任何命令行命令)

在我们的例子里可以使用这样的用户名(都在一行):

3)下面的例子创建一个asp脚本执行任意命令:

需要注意的很重要的一点是Windows NT4,IIS4平台asp脚本将会以'system'的帐号运行,而在IIS5他们会以低权限的IWAM_xxx帐号运行。

这当然也可以在我们的例子里使用,通过指定下面的'username'(注意例子不只是注入一段脚本,同时也以'admin'的身份登陆了程序)

传统的认识是如果ASP程序使用了数据库系统的存储过程,那么就不可能SQL注入了。这句话不完全对,这依赖于ASP脚本调用存储过程的方式。

本质上,一个带参数的查询执行了,用户提供的参数就被安全的传给查询,SQL注入就不可能了。但是,如果攻击者可以对无数据部分的查询语句施加任何影响,他们仍然可能控制数据库。

1. 如果ASP脚本创建了一个提交给服务器的SQL查询语句,这是很容易被SQL注入的,即使它使用了存储过程。
2. 如果ASP脚本使用了封装传递参数给存储过程的过程对象(如ADO command对象,和参数集合一起使用的)那么它通常就很安全了,但是这还要取决于对象的执行。

明显的,最好习惯于验证所有的用户输入,因为新的攻击技术会不停的涌现。

为了说明存储过程查询的注入,运行下面的SQL语句:

任何附加语句在存储过程执行后还是可以执行。

一个应用程序通常过滤单引号,另一方面限制用户的输入,比如限制长度。

在这里,我们将讨论一些绕过一些明显的SQL注入防范的和长度限制的技巧。

有时候,开发人员可能已经通过过滤单引号来保护应用程序,比如用VBScript的'replace'函数:

不可否认,这会阻止所有的对我们上面给出的对示例站点的攻击,删除';'字符也会起作用。但是,在一个大的程序里一些用户输入可能被假定为数值型。这些值没有限制,提供了很多可以注入的地方。

如果攻击者希望创建一个字符串值而不使用引号,他们可以用'char'函数。例如:

它是一个往表里插入字符的不带引号的查询语句。

当然,如果攻击者使用一个数值型的用户名和密码的话,下面的语句也同样可以很好的执行:

因为SQL-Server自动将数值型的转换成'varchar'类型,类型转换是默认的。

即使一个程序总是过滤单引号,攻击者仍然可以先注入SQL作为数据存放在数据库里然后被程序再次使用。

比如,一个攻击者可能通过注册,创建一个用户名

程序正确的过滤了单引号,'insert'语句如下:

我们假设程序允许用户更改密码,ASP脚本在设置新的密码前先确认用户旧密码正确。代码可能这样写:

设置新密码的查询语句可能这样写的:

用户名为admin'--,上面的查询就变成了这样:

因此攻击者可以通过注册了一个名叫admin'--的用户来把admin的密码改成他们自己的。

这是个危险的问题,目前大部分的大型程序都试图过滤数据。最好的解决方法是拒绝非法输入,而不是简单的改变它。这有时候会导致一些问题,非法字符在某些地方是必要的,比如在名字带符号的情况:

从安全的角度,最好的解决办法是不允许出现单引号。如果这样不行,必须避免它们出现,这种情况下,最好保证所有要进入SQL语句的字符(包括从数据库里取出的字符)都被正确的处理过。

即使这样攻击依然可能实现:如果攻击者可以不经过程序而往系统插入数据。比如攻击者有一个email接口,或者有一个可以控制的错误记录数据库。最好总是验证所有的数据,包括系统里的数据,验证函数调用很简单,比如:

有时候输入对数据的长度加以限制会使攻击困难许多,这的确阻止了一些攻击,但一个很短的SQL语句也可能造成非常大的危害:

关闭SQL-Server,只用了12个字符。另一个例子:

如果长度限制是在字符串过滤后,另一个问题可能会发生。假设用户名被限制在16个字符之内,密码也被限制在16个字符之内,下面的用户名和密码结合可以执行'shutdown'命令:

原因是程序过滤用户名最后的单引号,但是字符串又被切回到16个字符,删除了过滤的单引号。结果是密码域可以包含一些SQL, 只要它以一个单引号开始,最后的查询会变成这样:

用户名在查询里就变成:

后面附上的SQL被执行。

SQL Server在sp_traceXXX系列的函数包含丰富审核接口,它可以记录任何数据库里的事件。这里我们特别感兴趣的是T-SQL事件,它记录了所有的SQL语句以及服务器上准备好的和已运行了的批处理。如果这个级别的审核开启的话,所有我们讨论的注入都将被记录下来有经验的数据库管理员将会看到所有发生的事情。但是如果攻击者附加下面的字符:

到一个Transact-SQL语句,这个审核记录如下:

这在所有的的T-SQL日志记录时都会发生,即使'sp_password'出现在注释中。这当然是在用户传递sp_password时有意隐藏用户的明文密码,但这对攻击者相当有用。

所以,为了隐藏所有的注入攻击者只需要在注释符'--'后面加一个字符串:

事实上一些执行了的SQL将被记录,但是查询字符串本身被强制不记录。

这部分讨论一些针对这些攻击的防范措施。输入验证已经讨论过了,一些代码也给出了,后面我们研究SQL-Server防范问题。

输入验证是一个很复杂的问题。一般在一个开发项目中它很少被注意,因为过度的验证往往使一个程序的某部分被打断,所以输入验证是个难题。输入验证往往不加到程序的功能里,因而在工期将至而赶程序时不会被人注意。

下面是关于验证的简单的讨论附示例代码,这个示例代码当然不能直接用在程序里,但可以很好的说明不同的策略。

各种数据验证的途径可以分类为以下几种:

1)整理数据使之变得有效
2)拒绝已知的非法输入
3)只接受已知的合法的输入

有很多概念上的问题;首先,开发者没有必要知道非法数据由什么组成,因为新形式的非法数据随时都可能产生。第二,改变数据会改变它的长度,这样会导致前面提到的问题。最后,还有需要对系统已有数据的重用的话有二次注入的问题.

解决方案2也会遇到和1的一些相似的问题,了解非法数据会过时,因为新的攻击技术也在发展。

解决方案3可能是三种方法中最好的,但是比较难于执行。

从安全角度来考虑可能最好多解决方法是把解决方案2和3结合起来只允许合法的输入,然后再寻找非法字符。

一个必须结合这两种途径的例子是带有连字符的名字的问题:

我们必须在合法输入里允许连字符号,但是也要明白字符串'--'在SQL-Server里意味着什么。

当数据整理结合了非法字符验证时另一个问题就会发生。假设我们应用“非法字符探测器”来探测'--','select'和'union'”后使用“数据整理过滤器”删除单引号,攻击者就可以指定这样的输入:

因为单引号被过滤器删除了,攻击者可以把单引号散布于它的已知的非法字符串里来躲避检查。

下面是一些验证的代码:

方法2-抵制已知的非法输入

方法3-只允许合法输入

符合所有标识符格式规则的标识符可以使用分隔符,也可以不使用分隔符。不符合常规标识符格式规则的标识符必须使用分隔符。

Microsoft SQL Server 2005 无法识别分隔开的变量名和存储过程参数。这些标识符的类型必须符合常规的规则。

带分隔符的标识符用于下列情况:

  • 在对象名称或对象名称的组成部分中使用保留字时。
    保留的关键字不应用作对象名称。从 SQL Server 早期版本升级的数据库可能包含一些标识符,这些标识符包括早期版本中未保留而在 SQL Server 2005 中保留的关键字。在可以改变对象名称之前,可以使用带分隔符的标识符引用对象。

  • 使用未列为限定标识符的字符时。
    SQL Server 允许在带分隔符的标识符中使用当前代码页中的任何字符。但是,不加选择地在对象名称中使用特殊字符会使 SQL 语句和脚本难以阅读和维护。例如,您可以创建名为 Employee] 的表,其中右方括号是名称的一部分。若要执行此操作,必须再使用两个方括号以避免因使用右方括号而出现问题,如下所示:

分隔符仅用于标识符。分隔符不能用于关键字,即使它们在 SQL Server 中标记为保留关键字。
  • 引用的标识符用双引号 (") 分隔开:

  • 括在括号中的标识符用方括号 ([ ]) 分隔开:

  • 双引号只能用于分隔标识符,不能用于分隔字符串。
    为保持与现有应用程序的兼容性,SQL Server 并没有严格遵循该规则。如果字符串未超过标识符的长度,则可以包含在双引号内。我们建议不要这样做。

  • 单引号只用于包含字符串,不能用于分隔标识符。
    如果字符串包含嵌入的单引号,则应在该嵌入的单引号前再添加一个单引号。例如:

如果命名对象时使用了带分隔符的标识符,且对象名称包含尾随空格,则 SQL Server 存储该名称时不包含尾随空格。
  • 引号不能用于分隔标识符,而必须将括号用作分隔符。

  • 单引号或双引号可用于包含字符串。
    如果使用双引号,嵌入的单引号将不必用两个单引号来表示。例如:

无论采用哪种 QUOTED_IDENTIFIER 设置,都可以使用括号分隔符。

带分隔符的标识符的规则

带分隔符的标识符的格式规则如下:

  • 带分隔符的标识符可以包含的字符数与常规标识符相同。可以包含 1 到 128 个字符,但不包括分隔符字符。本地临时表标识符最多可以包含 116 个字符。

  • 标识符的主体可以包含当前代码页内字符(分隔符本身除外)的任意组合。例如,带分隔符的标识符可以包含空格、对常规标识符有效的任何字符以及下列任一字符:

下面的示例使用带引号的标识符作为表名称和列名称。指定带分隔符的标识符的两种方法如下所示:

DELETE 均为保留关键字,因此每次访问对象时都必须分隔标识符。

如果 SET QUOTED_IDENTIFIER 选项不为 ON,除非使用括号分隔符,否则不能访问表和列。例如:

下列语句中使用了括号分隔符,可以访问表和列:

将标识符分隔为多个部分

当使用限定对象名称时,可能要分隔组成对象名称的多个标识符。必须分别分隔每个标识符。例如:

对于如何分隔 ODBC CALL 语句中由多部分组成的存储过程名,有一些特殊规则。有关详细信息,请参阅。

许多系统存储过程、函数和 DBCC 语句都把对象名称当作参数。其中一些参数接受由多部分组成的对象名称,另一些则只接受由单一部分组成的名称。接受由单一部分组成的名称还是由多部分组成的名称决定了 SQL Server 在内部如何分析和使用参数。

如果参数为单部分标识符,则可以按照以下方式指定名称:

对于由单一部分组成的名称,单引号内的字符串代表对象名称。如果在单引号内使用分隔符,则分隔符字符将被视为名称的一部分。

如果名称包含句点或常规分隔符所定义的字符集以外的其他字符,必须将对象名称包含在单引号、双引号或括号内。

由多部分组成的名称为限定名称,包含数据库或架构名称以及对象名称。将由多部分组成的名称用作参数时,SQL Server 要求将组成该名称的完整字符串包含在一组单引号内。

如果各个名称部分需要使用分隔符,则名称的每一部分都应根据要求单独分隔。例如,如果名称某部分包含句点、双引号、左括号或右括号,则需要使用括号和双引号来分隔该部分。用单引号包含整个名称。

例如,表名 tab.one 包含一个句点。为防止该名称被解释为一个三部分的名称 dbo.tab.one,用分隔符分隔表名部分。

下面的示例介绍如何用双引号分隔同一表名:


我要回帖

更多关于 syntax包括什么 的文章

 

随机推荐