伟大的愿望迅雷 mp4:SQL 安全性: 新型 SQL 截断攻击和防御方法

来源:百度文库 编辑:中财网 时间:2024/04/30 15:51:57
本文讨论:
分隔标识符和字符串
实用的 T-SQL 函数
截断和修改攻击
找出 Bug 和漏洞
本文使用了以下技术:
SQL Server
目录
分隔标识符和字符串
T-SQL 函数
SQL 注入漏洞
通过截断进行修改
通过截断进行 SQL 注入
通过截断检测注入
利用 SQL 注入方法的漏洞攻击已经引起了广泛关注,因为这些漏洞能够穿过防火墙和入侵检测系统,从而破坏您的数据层。无论是第一级还是第二级注入攻击,如果您看一下基本的代码模式,它与其他任何注入攻击问题都类似,即您在构造语句时都使用了不受信任的数据。大多数开发人员已经开始通过在后端使用参数化 SQL 查询和存储过程来减少 Web 前端的这些漏洞,但有些情况下,开发人员仍使用动态构建的 SQL,例如根据用户输入或为 C/C++ 编写的应用程序构造数据定义语言 (DDL) 语句时。
在本文中我将讨论一些新观点,其结果可能会修改 SQL 语句或注入 SQL 代码,即使代码对分隔字符进行了转义。我首先介绍一些构建分隔标识符和 SQL 字符串的最佳实践,然后我将介绍攻击者注入 SQL 代码的几种新方法,以帮助您保护您的应用程序。
分隔标识符和字符串
在 SQL Server? 中,有两种字符串变量:唯一可识别 SQL 对象(如表、视图和存储过程)的 SQL 标识符,以及用于表示数据的字符串。分隔 SQL 标识符的方法与分隔数据字符串的方法不同。我们将讨论需要使用这些数据变量的动态 SQL 构建方法的最佳实践。
如果 SQL 对象名使用了关键字,或者对象名中包含了特殊字符,则您需要使用分隔标识符。假如您需要删除 my_dbreader 登录名,则可以执行以下语句:
双击代码全选1
DROP LOGIN my_dbreader
如果您试着删除一个使用 DROP 作为其名称(也是关键字)的登录名会怎样?如果您使用以下 SQL 语句,SQL Server 会返回一个语法错误。
双击代码全选1
DROP LOGIN DROP
如果您要删除像 my][dbreader 这样的登录名又会怎样?这也会引发语法错误。  在上面两个例子中,由于登录名为关键字或包含特殊字符,因此您需要提供一些开始和结束标记,以便 SQL Server 可以识别 SQL 语句中的对象名。
您可以使用双引号或方括号作为 SQL 标识符的分隔符,而在 QUOTED_IDENTIFIER 设置(一种基于连接的设置)启用时您可以只使用双引号。为简便起见,最好始终使用方括号。
要成功删除 DROP 登录名,您可以使用方括号来构造您的 SQL 语句:
双击代码全选1
DROP LOGIN [DROP]
但以下语句会怎样?双击代码全选1
DROP LOGIN [my][dbreader]
在这种特殊情况下,由于登录名 my][dbreader 中包含分隔字符,因此 SQL 会认为 [my] 是登录名,因为它被包含在方括号内。由于 [dbreader] 跟在登录名后面,因此该语句并不构成正确的 SQL 语句,会导致语法错误。您可以通过用另一个右方括号对上面的右方括号进行转义来解决这一问题。因此,如果您执行以下语句,SQL Server 将成功删除 my][dbreader 登录名:双击代码全选1
DROP LOGIN [my]][dbreader]
转义机制只是使右方括号的出现次数增加了一倍。您无需改动其他字符,包括左方括号。  准备分隔字符串与准备分隔 SQL 标识符类似,主要区别就是需要使用的分隔字符。在介绍与构建分隔字符串相似的规则之前,先来看以下几个例子。
假设您希望创建 dbreader 登录名,密码是 P@$$w0rd。您会使用以下 SQL 语句:
双击代码全选1
CREATE LOGIN [dbreader] WITH PASSWORD = 'P@$$w0rd'
在该语句中,P@$$w0rd 是由单引号分隔的字符串数据,因此 SQL 知道该字符串从哪里开始,到哪里结束。但如果字符串数据中包含单引号会怎样?SQL Server 会引发一个错误,因为该语句为无效语句:双击代码全选1
CREATE LOGIN [dbreader] WITH PASSWORD = 'P@$$'w0rd'
您需要对字符串中出现的所有单引号进行转义,构造有效的 SQL 语句:双击代码全选1
CREATE LOGIN [dbreader] WITH PASSWORD = 'P@$$''w0rd'
当您执行该语句时,SQL Server 将创建 dbreader 登录名,密码为 P@$$'w0rd。  您还可以使用双引号作为分隔符,但正如我前面提到的,这种方法是否成功完全取决于 QUOTED_IDENTIFIER 设置是否已开启。因此,最好始终使用单引号作为字符串的分隔符。
T-SQL 函数
可以看出,处理标识符和字符串的规则相对比较简单,如果您预先知道该字符串,可以手动对其进行分隔。但如果您要根据用户输入构建动态的 T-SQL 语句,该怎么办?您需要通过自动的方法来完成。两种 T-SQL 函数可帮您准备分隔字符串,它们是 QUOTENAME 和 REPLACE。
QUOTENAME 会返回一个 Unicode 字符串,并添加了分隔符,以使该输入字符串成为有效标识符。QUOTENAME 函数使用以下语法:
双击代码全选1
QUOTENAME ( 'string' [ , 'delimiter' ] )
您可以将要分隔的字符串和一个用作分隔符的单字符字符串传给 QUOTENAME。分隔符可以是方括号、单引号或双引号。  此函数主要用于准备分隔 SQL 标识符,因此它只接受 sysname 类型,在 SQL Server 中为 nvarchar(128)。您还可以使用此函数准备分隔 SQL 字符串,但由于参数长度的限制,因此它支持的字符串长度最多为 128 个字符(在这一点上,REPLACE 函数可发挥其用途)。图 1 显示了 sp_addlogin 如何使用 QUOTENAME 来准备分隔登录名和密码字符串。可以看出,由于 @loginname 和 @passwd 均为 sysname 类型,因此可使用 QUOTENAME 函数准备分隔 SQL 标识符和分隔字符串。因此,即使有人传递的是 @loginname = 'my[]dbreader' 和 @passwd = 'P@$$''w0rd',也不会有任何 SQL 注入机会,因为 QUOTENAME 对分隔字符进行了适当转义:
双击代码全选1
create login [my[]]dbreader] with password = 'P@$$''w0rd'
Figure1Delimiting Strings with QUOTENAME
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create procedure sys.sp_addlogin
@loginame    sysname
,@passwd     sysname = Null
,@defdb     sysname = ‘master’
,@deflanguage  sysname = Null
,@sid      varbinary(16) = Null
,@encryptopt   varchar(20) = Null
AS
-- SETUP RUNTIME OPTIONS / DECLARE VARIABLES --
-- some code ----
set @exec_stmt = ‘create login ‘ + quotename(@loginame, ‘[‘)
if @passwd is null
select @passwd = ‘‘
if (@encryptopt is null)
set @exec_stmt = @exec_stmt + ‘ with password = ‘ +
quotename(@passwd, ‘‘‘‘)
else
-- some code
GO
REPLACE 函数会将某个给定字符串的所有出现之处全部替换为指定的替代字符串。与 QUOTENAME 不同,REPLACE 函数对其接受的参数没有长度限制:
双击代码全选1
REPLACE ( 'string1' , 'string2' , 'string3' )
REPLACE 带有三个字符串:string1 是要编辑的表达式,string2 是 string1 中要被替换的项,string3 是用于取代 string2 的项。任何字符串表达式都可由字符或二进制数据组成。
要准备分隔 SQL 字符串,您可以使用 REPLACE 使单引号的出现次数增加一倍,但您需要手动添加分隔符(开始和结束的单引号)。图 2 显示了 sp_attach_single_file_db 如何使用此函数准备一个文件的已转义的物理名称。由于 @physname 是 nvarchar(260),因此您无法使用 QUOTENAME 准备分隔字符串,这就是为何要使用 REPLACE 的原因。因此,即使有人传递带单引号的字符串,他们也无法打破 SQL 语句,注入任何 SQL 代码。
Figure2Delimiting Strings with REPLACE
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create procedure sys.sp_attach_single_file_db
@dbname sysname,
@physname nvarchar(260)
as
declare @execstring nvarchar (4000)
-- some code --
select @execstring = ‘CREATE DATABASE ‘
+ quotename( @dbname , ‘[‘)
+ ‘ ON (FILENAME =‘
+ ‘‘‘‘
+ REPLACE(@physname,N’’’’,N’’’’’’)
+ ‘‘‘‘
+ ‘ ) FOR ATTACH’
EXEC (@execstring)
-- some code --
GO
SQL 注入漏洞
接下来介绍存储过程,它可在验证了当前密码后更改用户帐户的密码(参见图 3)。
Figure3Changing a Password
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
CREATE PROCEDURE sp_setPassword
@username varchar(25),
@old varchar(25),
@new varchar(25)
AS
DECLARE @command varchar(100)
SET @command=
‘update Users set password=‘‘‘ + @new +
‘‘‘ where username=‘‘‘ + @username +
‘‘‘ AND password=‘‘‘ + @old + ‘‘‘‘
EXEC (@command)
GO
快速浏览存储过程,会发现没有任何参数对单引号进行转义,这同样容易受到 SQL 注入攻击。攻击者可以传递几个特定的参数,并将 SQL 语句修改为:
双击代码全选1
2
update Users set password='NewP@ssw0rd'
where username='admin' --' and password='dummy'
结果是,无需实际密码即可设置管理员帐户(或任何已知的帐户)的密码。在 T-SQL 函数中,您可以通过使用 REPLACE 或 QUOTENAME 函数修复此代码。图 4 显示了使用 REPLACE 函数后正确的代码。Figure4Using REPLACE to Avoid Injection
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE PROCEDURE sp_setPassword
@username varchar(25),
@old varchar(25),
@new varchar(25)
AS
-- Declare variables.
DECLARE @command varchar(100)
-- Construct the dynamic SQL
SET @command=
‘update Users set password=‘‘‘ + REPLACE(@new, ‘‘‘‘, ‘‘‘‘‘‘) + ‘‘‘‘ +
‘ where username=‘‘‘ + REPLACE(@username, ‘‘‘‘, ‘‘‘‘‘‘) + ‘‘‘‘ +
‘ AND password = ‘‘‘ + REPLACE(@old, ‘‘‘‘, ‘‘‘‘‘‘) + ‘‘‘‘
-- Execute the command.
EXEC (@command)
GO
可以看出,REPLACE 会将参数中所有单引号的出现次数都增加一倍。因此,如果攻击者传递相同的参数,该语句会变为:
双击代码全选1
2
update Users set password='NewP@ssw0rd'
where username='admin''--' and password='dummy'
这样就不容易受到通常的 SQL 注入问题的影响了。
通过截断进行修改
如果您仔细留意上面显示的存储过程,您会发现 @command 变量只能存放 100 个字符,但当 25 个字符都为单引号时,这些字符的每个变量经过 REPLACE 函数处理后可返回 50 个字符。如果变量没有足够大的缓冲区,SQL Server 2000 SP4 和 SQL Server 2005 SP1 会自行截断数据。这就为攻击者提供了截断命令字符串的机会。
在此例中,如果有人可以在 username='username' 表达式后截断命令,那么无需知道已知用户的当前密码,就可更改其帐户的密码。
假设攻击者知道 administrator 用户名存在于 Web 应用程序中(这可以是任何用户帐户)。攻击者需要提供长度为 41 个字符的新密码,以使命令的长度足以被适当截断 — 之所以是 41 个字符,是因为在用于命令的 100 个字符中,27 个字符用于更新语句,17 个字符用于 where 子句,13 个字符用于“administrator”,2 个字符用于新密码前后的单引号。
攻击者只能传递 25 个字符作为新密码。但他可以通过传递单引号避开这一限制,因为 REPLACE 函数会使单引号数量增加一倍。因此,通过传递 18 个单引号、1 个大写字母、1 个符号、2 个小写字母和 1 个数字,攻击者就可以截断 where username='administrator' 表达式后面的命令了。如果攻击者将 ''''''''''''''''''!Abb1 传递给 @new 参数,并将 administrator 作为用户名参数,那么 @command 就会变成:
双击代码全选1
2
update Users set password=
'''''''''''''''''''''''''''''''''''''!Abb1' where username='administrator'
图 5 使用 QUOTENAME 而非 REPLACE。上面的例子和此例的唯一不同在于,在上例中,开发人员为用户名、新密码和旧密码添加了单引号分隔符,而在此例中,由 QUOTENAME 函数添加。由于用户提供的数据没有变化,因此上例中使用的同一攻击字符串仍然可以被攻击者利用。图 6 是在中间层应用程序中编写的 C/C++ 函数的缩写版本,可实现相同功能。它容易受到相同的攻击。
Figure6Truncation Problems in C++
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
DWORD ChangePassword(char* psUserName, char* psOld, char* psNew)
{
char* psEscapedUserName = NULL;
char* psEscapedOldPW = NULL;
char* psEscapedNewPW = NULL;
char szSQLCommand[100];
HRESULT hr=0;
// Input Validation
...
// Calculate and allocate the new buffer with length
// userdatalen*2 + 1
// Escape all single quotes with double quotes
...
//Construct the query
hr = StringCchPrintf(szSQLCommand, sizeof(szSQLCommand)/sizeof(char),
"Update Users set password=‘%s’ where username=‘%s’"
"AND password=‘%s’,
psEscapedNewPW, psEscapedUserName, psEscapedOldPW);
if (S_OK != hr)
{
// handle error cases
}
// Execute and return
}
Figure5Using QUOTENAME to Avoid Injection
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE PROCEDURE sp_setPassword
@username varchar(25),
@old varchar(25),
@new varchar(25)
AS
-- Declare variables.
DECLARE @command varchar(100)
-- In the following statement, we will need 43 characters
-- to set an administrator password without knowing its current password.
-- 100 - 26 - 16 - 15 = 43 (26 for update stmt, 16 for where clause,
-- 15 for ‘administrator’). But @new only takes 25 characters, which we
-- can get around by using single quotes. So one can pass the following
-- parametes and set admin password. @new = 18 single quotes, 1 Capital
-- letter, 1 symbol, 2 small case letters, 1 digit
-- @username = administrator
-- @command becomes
-- update Users set password=‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘‘!Abb1’
-- where username=‘administrator’
SET @command= ‘update Users set password=‘ + QUOTENAME(@new,’’’’) +
‘ where username=‘ + QUOTENAME(@username,’’’’) + ‘ AND password = ‘ + QUOTENAME(@old,’’’’)
-- Execute the command.
EXEC (@command)
GO
通过截断进行 SQL 注入
图 7 显示了相同代码的另一变体,但可使用单独的变量进行修复。可以看出,此代码将转义后的字符串存放在单独的变量中,而且 @command 有足够的缓冲区来存放整个字符串。@escaped_username、@escaped_oldpw 和 @escaped_newpw 被声明为 varchar(25),但如果 @username、@old 和 @new 中的所有字符是 25 个单引号,则它们需要存放 50 个字符。这就为截断已转义的字符串创造了机会。
Figure7Using Seperate Variables to Avoid Injection
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE PROCEDURE sp_setPassword
@username varchar(25),
@old varchar(25),
@new varchar(25)
AS
-- Declare variables.
DECLARE @escaped_username varchar(25)
DECLARE @escaped_oldpw varchar(25)
DECLARE @escaped_newpw varchar(25)
DECLARE @command varchar(250)
SET @escaped_username = REPLACE(@username, ‘‘‘‘, ‘‘‘‘‘‘)
SET @escaped_oldpw = REPLACE(@old, ‘‘‘‘, ‘‘‘‘‘‘)
SET @escaped_newpw = REPLACE(@new, ‘‘‘‘, ‘‘‘‘‘‘)
SET @command =
‘update Users set password=‘‘‘ + @escaped_newpw + ‘‘‘‘ +
‘ where username=‘‘‘ + @escaped_username + ‘‘‘‘ +
‘ AND password = ‘‘‘ + @escaped_oldpw + ‘‘‘‘
EXEC (@command)
GO
攻击者可以传递 123...n' 作为新密码,其中 n 是第 24 个字符,使 @escaped_newpw 也成为 123...n'(REPLACE 函数返回的第二个单引号字符会被截断),使最后的查询如下所示,攻击者可以通过用户名字段注入代码,从而利用此查询:
双击代码全选1
2
update users set password='123...n''
where username='
这种代码模式更危险,因为这为注入 SQL 代码(而不仅仅是截断现有 SQL)提供了机会。  图 8 提供了使用 QUOTENAME 函数而非 REPLACE 的同一变体的另一个例子。由于 QUOTENAME 函数要添加分隔符,因此负载会有所不同,但仍旧容易受到 SQL 注入攻击。
Figure8Using QUOTENAME with Seperate Variables
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ALTER PROCEDURE sp_setPassword
@username varchar(25),
@old varchar(25),
@new varchar(25)
AS
-- Declare variables.
DECLARE @quoted_username varchar(25)
DECLARE @quoted_oldpw varchar(25)
DECLARE @quoted_newpw varchar(25)
DECLARE @command varchar(250)
-- In the following statements, all the variables can only hold
-- 25 characters, but quotename() will return 52 characters when all
-- the characters are single quotes.
SET @quoted_username = QUOTENAME(@username, ‘‘‘‘)
SET @quoted_oldpw = QUOTENAME(@old, ‘‘‘‘)
SET @quoted_newpw = QUOTENAME(@new, ‘‘‘‘)
-- By passing the new password as 123...n where n is 24th character,
-- @quoted_newpw becomes ‘123..n
-- Observe carefully that there is no trailing single quote as it gets
-- truncated.
-- So the final query becomes something like this
-- update users set password=‘123...n where username=‘ -- here using Username>
SET @command= ‘update Users set password=‘ + @quoted_newpw +
‘ where username=‘ + @quoted_username +
‘ AND password = ‘ + @quoted_oldpw
EXEC (@command)
GO
在此例中,代码将分隔后的字符串存放在单独的变量中,而且 @command 有足够的缓冲区来存放整个命令字符串。正如上例所示,问题在于被引用的变量 @quoted_username、@quoted_oldpw 和 @quoted_newpw。它们都被声明为 varchar(25),但如果 @username、@old 和 @new 中的所有字符是 25 个单引号,则它们需要存放 52 个字符。(QUOTENAME 还将添加开始和结束的分隔符。)这就为攻击者截断已分隔的字符串创造了机会。
攻击者可以传递 123...n(其中 n 是第 24 个字符)作为新密码,使 @escaped_newpw 也成为 '123...n(开始的单引号由 QUOTENAME 添加),使最后的查询如下所示,攻击者可以通过用户名字段注入代码,从而利用此查询:
双击代码全选1
2
3
update users set
password='123...n where
username='
图 9 是 C/C++ 中此代码的缩写版本,可实现相同功能。它同样容易受到相同攻击。Figure9Variable Truncation Issues in C++
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DWORD ChangePassword(char* psUserName, char* psOld, char* psNew)
{
char szEscapedUserName[26];
char szEscapedOldPW[26];
char szEscapedNewPW[26];
char szSQLCommand[250];
// Input Validation
// Escape User supplied data
Replace(psUserName, "’", "’’", szEscapedUserName,
sizeof(szEscapedUserName));
Replace(psPassword, "’", "’’", szEscapedOldPW,
sizeof(szEscapedOldPW));
Replace(psPassword, "’", "’’", szEscapedNewPW,
sizeof(szEscapedNewPW));
// Construct the query
StringCchPrintf(szSQLCommand, sizeof(szSQLCommand)/sizeof(char),
"Update Users set password=‘%s’ where username=‘%s’"
"AND password=‘%s’,
szEscapedNewPW, szEscapedUserName,szEscapedOldPW);
// Execute and return
}
尽管我在演示中使用的是 T-SQL 代码,但实际上您不需要使用动态 SQL 来构造数据操作语言 (DML) 语句,因此大多数包含 DML 代码的应用程序不易受到这些问题的困扰。
下面,我们来看看另一个根据用户输入构造动态 DDL 语句的例子,如图 10 所示。就像前面的例子一样,以下语句也存在截断问题:
双击代码全选1
2
set @escaped_oldpw = quotename(@old, '''')
set @escaped_newpw = quotename(@new, '''')
攻击者通过传递 @new = '123...'(其中从第 127 个字符(无单引号)开始是 @old = '; SQL Injection'),会使 SQL 语句如下所示:双击代码全选1
2
3
alter login [loginname]
with password = '123... old_password = '; SQL
Injection
Figure10Creating a Dynamic DDL Statement
双击代码全选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create procedure sys.sp_password
@old sysname = NULL,    -- the old (current) password
@new sysname,        -- the new password
@loginame sysname = NULL  -- user to change password on
as
-- SETUP RUNTIME OPTIONS / DECLARE VARIABLES --
set nocount on
declare @exec_stmt nvarchar(4000)
declare @escaped_oldpw sysname
declare @escaped_newpw sysname
set @escaped_oldpw = quotename(@old, ‘‘‘‘)
set @escaped_newpw = quotename(@new, ‘‘‘‘)
set @exec_stmt = ‘alter login ‘ + quotename(@loginame) +
‘ with password = ‘ + @escaped_newpw + ‘ old_password = ‘ +
@escaped_old
exec (@exec_stmt)
if @@error <> 0
return (1)
-- RETURN SUCCESS --
return (0)  -- sp_password
尽管存储过程更可能出现这些问题,但并非所有存储过程都会导致安全漏洞。接下来介绍哪些存储过程需要仔细审查。
在 SQL Server 中,默认情况下,所有存储过程都在调用方的环境下执行。因此,即使某个过程存在 SQL 注入问题,对该过程具有执行权限的恶意的本地用户也无法提高其权限,并且注入的代码会在其环境下执行。但是如果您有内部维护脚本,作为计算机所有者或某个特定用户可以执行该脚本,那么调用方就可以在不同用户环境下执行代码,并将其权限提升为该用户的权限。
所有截断问题肯定都是 Bug,但它们不一定是安全漏洞。但最好还是修复这些问题,因为您并不知道将来谁会找出这些问题并对其加以利用。
您可以采取其他措施减少您的 SQL 代码中的注入漏洞。首先,在存储过程中避免使用动态 SQL 来构造 DML 语句。如果您无法避免使用动态 SQL,那么可以使用 sp_executesql。第二,正如本文所举的例子中说明的,您需要正确计算缓冲区的长度。最后,在 C/C++ 代码中,检查字符串运算返回值,并查看字符串是否已截断,如果已截断,则相应的结果错误。参见提要栏“漏洞检测方法”,了解您可以采取的措施的摘要。